Merge branch 'dev'

This commit is contained in:
microwavedcola1 2022-12-16 16:38:33 +01:00
commit 246b3a351d
41 changed files with 2815 additions and 2024 deletions

3
Cargo.lock generated
View File

@ -1118,10 +1118,12 @@ dependencies = [
"anchor-lang",
"anchor-spl",
"anyhow",
"async-trait",
"base64 0.13.1",
"bincode",
"fixed",
"fixed-macro",
"futures 0.3.25",
"itertools 0.10.5",
"log 0.4.17",
"mango-v4",
@ -1132,6 +1134,7 @@ dependencies = [
"serum_dex 0.5.6",
"shellexpand",
"solana-account-decoder",
"solana-address-lookup-table-program",
"solana-client",
"solana-sdk",
"spl-associated-token-account",

View File

@ -119,7 +119,8 @@ impl Rpc {
}
}
fn main() -> Result<(), anyhow::Error> {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
@ -137,7 +138,7 @@ fn main() -> Result<(), anyhow::Error> {
num
} else {
// find free account_num
let accounts = MangoClient::find_accounts(&client, group, &owner)?;
let accounts = MangoClient::find_accounts(&client, group, &owner).await?;
if accounts.is_empty() {
0
} else {
@ -149,14 +150,9 @@ fn main() -> Result<(), anyhow::Error> {
+ 1
}
};
let (account, txsig) = MangoClient::create_account(
&client,
group,
&owner,
&owner,
account_num,
&cmd.name,
)?;
let (account, txsig) =
MangoClient::create_account(&client, group, &owner, &owner, account_num, &cmd.name)
.await?;
println!("{}", account);
println!("{}", txsig);
}
@ -165,8 +161,8 @@ fn main() -> Result<(), anyhow::Error> {
let account = client::pubkey_from_cli(&cmd.account);
let owner = client::keypair_from_cli(&cmd.owner);
let mint = client::pubkey_from_cli(&cmd.mint);
let client = MangoClient::new_for_existing_account(client, account, owner)?;
let txsig = client.token_deposit(mint, cmd.amount)?;
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client.token_deposit(mint, cmd.amount).await?;
println!("{}", txsig);
}
Command::JupiterSwap(cmd) => {
@ -175,14 +171,16 @@ fn main() -> Result<(), anyhow::Error> {
let owner = client::keypair_from_cli(&cmd.owner);
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
let client = MangoClient::new_for_existing_account(client, account, owner)?;
let txsig = client.jupiter_swap(
input_mint,
output_mint,
cmd.amount,
cmd.slippage,
client::JupiterSwapMode::ExactIn,
)?;
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client
.jupiter_swap(
input_mint,
output_mint,
cmd.amount,
cmd.slippage,
client::JupiterSwapMode::ExactIn,
)
.await?;
println!("{}", txsig);
}
Command::GroupAddress { creator, num } => {

View File

@ -11,8 +11,10 @@ anchor-client = { path = "../anchor/client" }
anchor-lang = { path = "../anchor/lang" }
anchor-spl = { path = "../anchor/spl" }
anyhow = "1.0"
async-trait = "0.1.52"
fixed = { version = "=1.11.0", features = ["serde", "borsh"] }
fixed-macro = "^1.1.1"
futures = "0.3.25"
itertools = "0.10.3"
mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client"] }
pyth-sdk-solana = "0.1.0"
@ -21,6 +23,7 @@ shellexpand = "2.1.0"
solana-account-decoder = "~1.14.9"
solana-client = "~1.14.9"
solana-sdk = "~1.14.9"
solana-address-lookup-table-program = "~1.14.9"
spl-associated-token-account = "1.0.3"
thiserror = "1.0.31"
log = "0.4"
@ -29,4 +32,4 @@ tokio = { version = "1", features = ["full"] }
serde = "1.0.141"
serde_json = "1.0.82"
base64 = "0.13.0"
bincode = "1.3.3"
bincode = "1.3.3"

View File

@ -6,15 +6,22 @@ use anyhow::Context;
use anchor_client::ClientError;
use anchor_lang::AccountDeserialize;
use solana_client::rpc_client::RpcClient;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::pubkey::Pubkey;
use mango_v4::state::MangoAccountValue;
#[async_trait::async_trait(?Send)]
pub trait AccountFetcher: Sync + Send {
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
fn fetch_program_accounts(
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
async fn fetch_raw_account_lookup_table(
&self,
address: &Pubkey,
) -> anyhow::Result<AccountSharedData> {
self.fetch_raw_account(address).await
}
async fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
@ -22,35 +29,37 @@ pub trait AccountFetcher: Sync + Send {
}
// Can't be in the trait, since then it would no longer be object-safe...
pub fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
pub async fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
fetcher: &dyn AccountFetcher,
address: &Pubkey,
) -> anyhow::Result<T> {
let account = fetcher.fetch_raw_account(address)?;
let account = fetcher.fetch_raw_account(address).await?;
let mut data: &[u8] = &account.data();
T::try_deserialize(&mut data)
.with_context(|| format!("deserializing anchor account {}", address))
}
// Can't be in the trait, since then it would no longer be object-safe...
pub fn account_fetcher_fetch_mango_account(
pub async fn account_fetcher_fetch_mango_account(
fetcher: &dyn AccountFetcher,
address: &Pubkey,
) -> anyhow::Result<MangoAccountValue> {
let account = fetcher.fetch_raw_account(address)?;
let account = fetcher.fetch_raw_account(address).await?;
let data: &[u8] = &account.data();
MangoAccountValue::from_bytes(&data[8..])
.with_context(|| format!("deserializing mango account {}", address))
}
pub struct RpcAccountFetcher {
pub rpc: RpcClient,
pub rpc: RpcClientAsync,
}
#[async_trait::async_trait(?Send)]
impl AccountFetcher for RpcAccountFetcher {
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
self.rpc
.get_account_with_commitment(address, self.rpc.commitment())
.await
.with_context(|| format!("fetch account {}", *address))?
.value
.ok_or(ClientError::AccountNotFound)
@ -58,7 +67,7 @@ impl AccountFetcher for RpcAccountFetcher {
.map(Into::into)
}
fn fetch_program_accounts(
async fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
@ -78,7 +87,10 @@ impl AccountFetcher for RpcAccountFetcher {
},
with_context: Some(true),
};
let accs = self.rpc.get_program_accounts_with_config(program, config)?;
let accs = self
.rpc
.get_program_accounts_with_config(program, config)
.await?;
// convert Account -> AccountSharedData
Ok(accs
.into_iter()
@ -121,18 +133,19 @@ impl<T: AccountFetcher> CachedAccountFetcher<T> {
}
}
#[async_trait::async_trait(?Send)]
impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
let mut cache = self.cache.lock().unwrap();
if let Some(account) = cache.accounts.get(address) {
return Ok(account.clone());
}
let account = self.fetcher.fetch_raw_account(address)?;
let account = self.fetcher.fetch_raw_account(address).await?;
cache.accounts.insert(*address, account.clone());
Ok(account)
}
fn fetch_program_accounts(
async fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
@ -147,7 +160,8 @@ impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
}
let accounts = self
.fetcher
.fetch_program_accounts(program, discriminator)?;
.fetch_program_accounts(program, discriminator)
.await?;
cache
.keys_for_program_and_discriminator
.insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect());

View File

@ -11,7 +11,7 @@ use mango_v4::state::{MangoAccount, MangoAccountValue};
use anyhow::Context;
use solana_client::rpc_client::RpcClient;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::clock::Slot;
use solana_sdk::pubkey::Pubkey;
@ -19,7 +19,7 @@ use solana_sdk::signature::Signature;
pub struct AccountFetcher {
pub chain_data: Arc<RwLock<ChainData>>,
pub rpc: RpcClient,
pub rpc: RpcClientAsync,
}
impl AccountFetcher {
@ -55,16 +55,19 @@ impl AccountFetcher {
}
// fetches via RPC, stores in ChainData, returns new version
pub fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
pub async fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
&self,
address: &Pubkey,
) -> anyhow::Result<T> {
self.refresh_account_via_rpc(address)?;
self.refresh_account_via_rpc(address).await?;
self.fetch(address)
}
pub fn fetch_fresh_mango_account(&self, address: &Pubkey) -> anyhow::Result<MangoAccountValue> {
self.refresh_account_via_rpc(address)?;
pub async fn fetch_fresh_mango_account(
&self,
address: &Pubkey,
) -> anyhow::Result<MangoAccountValue> {
self.refresh_account_via_rpc(address).await?;
self.fetch_mango_account(address)
}
@ -76,10 +79,11 @@ impl AccountFetcher {
.clone())
}
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
pub async fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
let response = self
.rpc
.get_account_with_commitment(address, self.rpc.commitment())
.await
.with_context(|| format!("refresh account {} via rpc", address))?;
let slot = response.context.slot;
let account = response
@ -100,8 +104,8 @@ impl AccountFetcher {
}
/// Return the maximum slot reported for the processing of the signatures
pub fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
let statuses = self.rpc.get_signature_statuses(signatures)?.value;
pub async fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
let statuses = self.rpc.get_signature_statuses(signatures).await?.value;
Ok(statuses
.iter()
.map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0))
@ -110,7 +114,7 @@ impl AccountFetcher {
}
/// Return success once all addresses have data >= min_slot
pub fn refresh_accounts_via_rpc_until_slot(
pub async fn refresh_accounts_via_rpc_until_slot(
&self,
addresses: &[Pubkey],
min_slot: Slot,
@ -126,7 +130,7 @@ impl AccountFetcher {
min_slot
);
}
let data_slot = self.refresh_account_via_rpc(address)?;
let data_slot = self.refresh_account_via_rpc(address).await?;
if data_slot >= min_slot {
break;
}
@ -137,15 +141,29 @@ impl AccountFetcher {
}
}
#[async_trait::async_trait(?Send)]
impl crate::AccountFetcher for AccountFetcher {
fn fetch_raw_account(
async fn fetch_raw_account(
&self,
address: &Pubkey,
) -> anyhow::Result<solana_sdk::account::AccountSharedData> {
self.fetch_raw(&address)
self.fetch_raw(address)
}
fn fetch_program_accounts(
async fn fetch_raw_account_lookup_table(
&self,
address: &Pubkey,
) -> anyhow::Result<AccountSharedData> {
// Fetch data via RPC if missing: the chain data updater doesn't know about all the
// lookup talbes we may need.
if let Ok(alt) = self.fetch_raw(address) {
return Ok(alt);
}
self.refresh_account_via_rpc(address).await?;
self.fetch_raw(address)
}
async fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,24 @@
use std::collections::HashMap;
use anchor_client::{Client, ClientError, Cluster, Program};
use anchor_client::ClientError;
use anchor_lang::__private::bytemuck;
use mango_v4::state::{
MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex,
TokenIndex,
Group, MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market,
Serum3MarketIndex, TokenIndex,
};
use fixed::types::I80F48;
use futures::{stream, StreamExt, TryStreamExt};
use itertools::Itertools;
use crate::gpa::*;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::Account;
use solana_sdk::instruction::AccountMeta;
use solana_sdk::signature::Keypair;
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
use solana_sdk::pubkey::Pubkey;
#[derive(Clone)]
pub struct TokenContext {
@ -64,6 +65,8 @@ pub struct MangoGroupContext {
pub perp_markets: HashMap<PerpMarketIndex, PerpMarketContext>,
pub perp_market_indexes_by_name: HashMap<String, PerpMarketIndex>,
pub address_lookup_tables: Vec<Pubkey>,
}
impl MangoGroupContext {
@ -94,17 +97,11 @@ impl MangoGroupContext {
self.perp(perp_market_index).address
}
pub fn new_from_rpc(
group: Pubkey,
cluster: Cluster,
commitment: CommitmentConfig,
) -> Result<Self, ClientError> {
let program =
Client::new_with_options(cluster, std::rc::Rc::new(Keypair::new()), commitment)
.program(mango_v4::ID);
pub async fn new_from_rpc(rpc: &RpcClientAsync, group: Pubkey) -> anyhow::Result<Self> {
let program = mango_v4::ID;
// tokens
let mint_info_tuples = fetch_mint_infos(&program, group)?;
let mint_info_tuples = fetch_mint_infos(rpc, program, group).await?;
let mut tokens = mint_info_tuples
.iter()
.map(|(pk, mi)| {
@ -124,7 +121,7 @@ impl MangoGroupContext {
// reading the banks is only needed for the token names and decimals
// FUTURE: either store the names on MintInfo as well, or maybe don't store them at all
// because they are in metaplex?
let bank_tuples = fetch_banks(&program, group)?;
let bank_tuples = fetch_banks(rpc, program, group).await?;
for (_, bank) in bank_tuples {
let token = tokens.get_mut(&bank.token_index).unwrap();
token.name = bank.name().into();
@ -133,11 +130,15 @@ impl MangoGroupContext {
assert!(tokens.values().all(|t| t.decimals != u8::MAX));
// serum3 markets
let serum3_market_tuples = fetch_serum3_markets(&program, group)?;
let serum3_market_tuples = fetch_serum3_markets(rpc, program, group).await?;
let serum3_markets_external = stream::iter(serum3_market_tuples.iter())
.then(|(_, s)| fetch_raw_account(rpc, s.serum_market_external))
.try_collect::<Vec<_>>()
.await?;
let serum3_markets = serum3_market_tuples
.iter()
.map(|(pk, s)| {
let market_external_account = fetch_raw_account(&program, s.serum_market_external)?;
.zip(serum3_markets_external.iter())
.map(|((pk, s), market_external_account)| {
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&market_external_account.data
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
@ -148,7 +149,7 @@ impl MangoGroupContext {
&s.serum_program,
)
.unwrap();
Ok((
(
s.market_index,
Serum3MarketContext {
address: *pk,
@ -163,12 +164,12 @@ impl MangoGroupContext {
coin_lot_size: market_external.coin_lot_size,
pc_lot_size: market_external.pc_lot_size,
},
))
)
})
.collect::<Result<HashMap<_, _>, ClientError>>()?;
.collect::<HashMap<_, _>>();
// perp markets
let perp_market_tuples = fetch_perp_markets(&program, group)?;
let perp_market_tuples = fetch_perp_markets(rpc, program, group).await?;
let perp_markets = perp_market_tuples
.iter()
.map(|(pk, pm)| {
@ -196,6 +197,14 @@ impl MangoGroupContext {
.map(|(i, p)| (p.market.name().to_string(), *i))
.collect::<HashMap<_, _>>();
let group_data = fetch_anchor_account::<Group>(rpc, &group).await?;
let address_lookup_tables = group_data
.address_lookup_tables
.iter()
.filter(|&&k| k != Pubkey::default())
.cloned()
.collect::<Vec<Pubkey>>();
Ok(MangoGroupContext {
group,
tokens,
@ -204,6 +213,7 @@ impl MangoGroupContext {
serum3_market_indexes_by_name,
perp_markets,
perp_market_indexes_by_name,
address_lookup_tables,
})
}
@ -331,9 +341,9 @@ fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey {
Pubkey::new(bytemuck::cast_slice(&d as &[_]))
}
fn fetch_raw_account(program: &Program, address: Pubkey) -> Result<Account, ClientError> {
let rpc = program.rpc();
rpc.get_account_with_commitment(&address, rpc.commitment())?
async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Account, ClientError> {
rpc.get_account_with_commitment(&address, rpc.commitment())
.await?
.value
.ok_or(ClientError::AccountNotFound)
}

View File

@ -1,18 +1,19 @@
use anchor_client::{ClientError, Program};
use anchor_lang::Discriminator;
use anchor_lang::{AccountDeserialize, Discriminator};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
use solana_account_decoder::UiAccountEncoding;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
use solana_sdk::pubkey::Pubkey;
pub fn fetch_mango_accounts(
program: &Program,
pub async fn fetch_mango_accounts(
rpc: &RpcClientAsync,
program: Pubkey,
group: Pubkey,
owner: Pubkey,
) -> Result<Vec<(Pubkey, MangoAccountValue)>, ClientError> {
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue)>> {
let config = RpcProgramAccountsConfig {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
@ -28,47 +29,103 @@ pub fn fetch_mango_accounts(
},
..RpcProgramAccountsConfig::default()
};
program
.rpc()
.get_program_accounts_with_config(&program.id(), config)?
rpc.get_program_accounts_with_config(&program, config)
.await?
.into_iter()
.map(|(key, account)| Ok((key, MangoAccountValue::from_bytes(&account.data[8..])?)))
.collect::<Result<Vec<_>, _>>()
}
pub fn fetch_banks(program: &Program, group: Pubkey) -> Result<Vec<(Pubkey, Bank)>, ClientError> {
program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
pub async fn fetch_anchor_account<T: AccountDeserialize>(
rpc: &RpcClientAsync,
address: &Pubkey,
) -> anyhow::Result<T> {
let account = rpc.get_account(address).await?;
Ok(T::try_deserialize(&mut (&account.data as &[u8]))?)
}
pub fn fetch_mint_infos(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, MintInfo)>, ClientError> {
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
async fn fetch_anchor_accounts<T: AccountDeserialize + Discriminator>(
rpc: &RpcClientAsync,
program: Pubkey,
filters: Vec<RpcFilterType>,
) -> anyhow::Result<Vec<(Pubkey, T)>> {
let account_type_filter =
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &T::discriminator()));
let config = RpcProgramAccountsConfig {
filters: Some([vec![account_type_filter], filters].concat()),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
..RpcAccountInfoConfig::default()
},
..RpcProgramAccountsConfig::default()
};
rpc.get_program_accounts_with_config(&program, config)
.await?
.into_iter()
.map(|(key, account)| Ok((key, T::try_deserialize(&mut (&account.data as &[u8]))?)))
.collect()
}
pub fn fetch_serum3_markets(
program: &Program,
pub async fn fetch_banks(
rpc: &RpcClientAsync,
program: Pubkey,
group: Pubkey,
) -> Result<Vec<(Pubkey, Serum3Market)>, ClientError> {
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
) -> anyhow::Result<Vec<(Pubkey, Bank)>> {
fetch_anchor_accounts::<Bank>(
rpc,
program,
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))],
)
.await
}
pub fn fetch_perp_markets(
program: &Program,
pub async fn fetch_mint_infos(
rpc: &RpcClientAsync,
program: Pubkey,
group: Pubkey,
) -> Result<Vec<(Pubkey, PerpMarket)>, ClientError> {
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
) -> anyhow::Result<Vec<(Pubkey, MintInfo)>> {
fetch_anchor_accounts::<MintInfo>(
rpc,
program,
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))],
)
.await
}
pub async fn fetch_serum3_markets(
rpc: &RpcClientAsync,
program: Pubkey,
group: Pubkey,
) -> anyhow::Result<Vec<(Pubkey, Serum3Market)>> {
fetch_anchor_accounts::<Serum3Market>(
rpc,
program,
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))],
)
.await
}
pub async fn fetch_perp_markets(
rpc: &RpcClientAsync,
program: Pubkey,
group: Pubkey,
) -> anyhow::Result<Vec<(Pubkey, PerpMarket)>> {
fetch_anchor_accounts::<PerpMarket>(
rpc,
program,
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))],
)
.await
}

View File

@ -1,10 +1,11 @@
use crate::{AccountFetcher, MangoGroupContext};
use anyhow::Context;
use futures::{stream, StreamExt, TryStreamExt};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
use mango_v4::state::MangoAccountValue;
pub fn new(
pub async fn new(
context: &MangoGroupContext,
account_fetcher: &impl AccountFetcher,
account: &MangoAccountValue,
@ -13,18 +14,18 @@ pub fn new(
let active_perp_len = account.active_perp_positions().count();
let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?;
let accounts = metas
.iter()
.map(|meta| {
let accounts: anyhow::Result<Vec<KeyedAccountSharedData>> = stream::iter(metas.iter())
.then(|meta| async {
Ok(KeyedAccountSharedData::new(
meta.pubkey,
account_fetcher.fetch_raw_account(&meta.pubkey)?,
account_fetcher.fetch_raw_account(&meta.pubkey).await?,
))
})
.collect::<anyhow::Result<Vec<_>>>()?;
.try_collect()
.await;
let retriever = FixedOrderAccountRetriever {
ais: accounts,
ais: accounts?,
n_banks: active_token_len,
n_perps: active_perp_len,
begin_perp: active_token_len * 2,

View File

@ -5,20 +5,21 @@ use serde::{Deserialize, Serialize};
pub struct QueryResult {
pub data: Vec<QueryRoute>,
pub time_taken: f64,
pub context_slot: String,
pub context_slot: u64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryRoute {
pub in_amount: u64,
pub out_amount: u64,
pub amount: u64,
pub other_amount_threshold: u64,
pub out_amount_with_slippage: Option<u64>,
pub swap_mode: String,
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)]
@ -28,22 +29,35 @@ pub struct QueryMarketInfo {
pub label: String,
pub input_mint: String,
pub output_mint: String,
pub in_amount: u64,
pub out_amount: u64,
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,
pub not_enough_liquidity: bool,
pub price_impact_pct: Option<f64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryFee {
pub amount: u64,
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 {

View File

@ -15,7 +15,7 @@ pub enum Direction {
/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order.
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
pub fn fetch_top(
pub async fn fetch_top(
context: &crate::context::MangoGroupContext,
account_fetcher: &impl AccountFetcher,
perp_market_index: PerpMarketIndex,
@ -25,20 +25,23 @@ pub fn fetch_top(
use std::time::{SystemTime, UNIX_EPOCH};
let now_ts: u64 = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_millis()
.as_secs()
.try_into()?;
let perp = context.perp(perp_market_index);
let perp_market =
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address).await?;
let oracle_acc = account_fetcher
.fetch_raw_account(&perp_market.oracle)
.await?;
let oracle_price = perp_market.oracle_price(
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
None,
)?;
let accounts =
account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?;
let accounts = account_fetcher
.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())
.await?;
let mut accounts_pnl = accounts
.iter()
@ -54,6 +57,7 @@ pub fn fetch_top(
return None;
}
let mut perp_pos = perp_pos.unwrap().clone();
perp_pos.settle_funding(&perp_market);
perp_pos.update_settle_limit(&perp_market, now_ts);
let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap();
let limited_pnl = perp_pos.apply_pnl_settle_limit(pnl, &perp_market);
@ -88,8 +92,9 @@ pub fn fetch_top(
} else {
I80F48::ZERO
};
let perp_settle_health =
crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health();
let perp_settle_health = crate::health_cache::new(context, account_fetcher, &acc)
.await?
.perp_settle_health();
let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() {
(*pnl).max(-perp_settle_health)
} else {

View File

@ -1,10 +1,9 @@
use std::{sync::Arc, time::Duration, time::Instant};
use std::{collections::HashSet, sync::Arc, time::Duration, time::Instant};
use crate::MangoClient;
use itertools::Itertools;
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
use client::prettify_client_error;
use futures::Future;
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex};
use solana_sdk::{
@ -93,79 +92,63 @@ pub async fn loop_update_index_and_rate(
let token_indices_clone = token_indices.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let token_names = token_indices_clone
let token_names = token_indices_clone
.iter()
.map(|token_index| client.context.token(*token_index).name.to_owned())
.join(",");
let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_price(1)];
for token_index in token_indices_clone.iter() {
let token = client.context.token(*token_index);
let banks_for_a_token = token.mint_info.banks();
let oracle = token.mint_info.oracle;
let mut ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenUpdateIndexAndRate {
group: token.mint_info.group,
mint_info: token.mint_info_address,
oracle,
instructions: solana_program::sysvar::instructions::id(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::TokenUpdateIndexAndRate {},
),
};
let mut banks = banks_for_a_token
.iter()
.map(|token_index| client.context.token(*token_index).name.to_owned())
.join(",");
.map(|bank_pubkey| AccountMeta {
pubkey: *bank_pubkey,
is_signer: false,
is_writable: true,
})
.collect::<Vec<_>>();
ix.accounts.append(&mut banks);
instructions.push(ix);
}
let pre = Instant::now();
let sig_result = client
.send_and_confirm_permissionless_tx(instructions)
.await;
let program = client.program();
let mut req = program.request();
req = req.instruction(ComputeBudgetInstruction::set_compute_unit_price(1));
for token_index in token_indices_clone.iter() {
let token = client.context.token(*token_index);
let banks_for_a_token = token.mint_info.banks();
let oracle = token.mint_info.oracle;
let mut ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenUpdateIndexAndRate {
group: token.mint_info.group,
mint_info: token.mint_info_address,
oracle,
instructions: solana_program::sysvar::instructions::id(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::TokenUpdateIndexAndRate {},
),
};
let mut banks = banks_for_a_token
.iter()
.map(|bank_pubkey| AccountMeta {
pubkey: *bank_pubkey,
is_signer: false,
is_writable: true,
})
.collect::<Vec<_>>();
ix.accounts.append(&mut banks);
req = req.instruction(ix);
}
let pre = Instant::now();
let sig_result = req.send().map_err(prettify_client_error);
if let Err(e) = sig_result {
log::info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names,
pre.elapsed().as_millis(),
e
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names,
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
if let Err(e) = sig_result {
log::info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names,
pre.elapsed().as_millis(),
e
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names,
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
}
}
@ -181,15 +164,17 @@ pub async fn loop_consume_events(
interval.tick().await;
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let mut event_queue: EventQueue =
client.program().account(perp_market.event_queue).unwrap();
let mut ams_ = vec![];
let find_accounts = || async {
let mut num_of_events = 0;
let mut event_queue: EventQueue = client
.client
.rpc_anchor_account(&perp_market.event_queue)
.await?;
// TODO: future, choose better constant of how many max events to pack
// TODO: future, choose better constant of how many max mango accounts to pack
let mut set = HashSet::new();
for _ in 0..10 {
let event = match event_queue.peek_front() {
None => break,
@ -198,100 +183,87 @@ pub async fn loop_consume_events(
match EventType::try_from(event.event_type)? {
EventType::Fill => {
let fill: &FillEvent = cast_ref(event);
if fill.maker == fill.taker {
ams_.push(AccountMeta {
pubkey: fill.maker,
is_signer: false,
is_writable: true,
});
} else {
ams_.push(AccountMeta {
pubkey: fill.maker,
is_signer: false,
is_writable: true,
});
ams_.push(AccountMeta {
pubkey: fill.taker,
is_signer: false,
is_writable: true,
});
}
set.insert(fill.maker);
set.insert(fill.taker);
}
EventType::Out => {
let out: &OutEvent = cast_ref(event);
ams_.push(AccountMeta {
pubkey: out.owner,
is_signer: false,
is_writable: true,
});
set.insert(out.owner);
}
EventType::Liquidate => {}
}
event_queue.pop_front()?;
num_of_events+=1;
num_of_events += 1;
}
if num_of_events == 0 {
return Ok(());
return Ok(None);
}
let pre = Instant::now();
let sig_result = client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpConsumeEvents {
group: perp_market.group,
perp_market: pk,
event_queue: perp_market.event_queue,
},
None,
);
ams.append(&mut ams_);
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpConsumeEvents { limit: 10 },
),
})
.send()
.map_err(prettify_client_error);
Ok(Some((set, num_of_events)))
};
if let Err(e) = sig_result {
log::info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
);
log::info!("{:?}", sig_result);
let event_info: anyhow::Result<Option<(HashSet<Pubkey>, u32)>> = find_accounts().await;
let (event_accounts, num_of_events) = match event_info {
Ok(Some(x)) => x,
Ok(None) => continue,
Err(err) => {
log::error!("preparing consume_events ams: {err:?}");
continue;
}
};
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
let mut event_ams = event_accounts
.iter()
.map(|key| -> AccountMeta {
AccountMeta {
pubkey: *key,
is_signer: false,
is_writable: true,
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
})
.collect::<Vec<_>>();
let pre = Instant::now();
let ix = Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpConsumeEvents {
group: perp_market.group,
perp_market: pk,
event_queue: perp_market.event_queue,
},
None,
);
ams.append(&mut event_ams);
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpConsumeEvents {
limit: 10,
}),
};
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
if let Err(e) = sig_result {
log::info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
);
log::info!("{:?}", sig_result);
}
}
}
@ -307,59 +279,39 @@ pub async fn loop_update_funding(
interval.tick().await;
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let pre = Instant::now();
let sig_result = client
.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpUpdateFunding {
group: perp_market.group,
perp_market: pk,
bids: perp_market.bids,
asks: perp_market.asks,
oracle: perp_market.oracle,
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpUpdateFunding {},
),
})
.send()
.map_err(prettify_client_error);
if let Err(e) = sig_result {
log::error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name(),
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
Ok(())
})
.await;
let pre = Instant::now();
let ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpUpdateFunding {
group: perp_market.group,
perp_market: pk,
bids: perp_market.bids,
asks: perp_market.asks,
oracle: perp_market.oracle,
},
None,
),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
};
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
if let Err(e) = sig_result {
log::error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name(),
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
}
}

View File

@ -62,7 +62,9 @@ enum Command {
Crank {},
Taker {},
}
fn main() -> Result<(), anyhow::Error> {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
@ -87,21 +89,19 @@ fn main() -> Result<(), anyhow::Error> {
Command::Taker { .. } => CommitmentConfig::confirmed(),
};
let mango_client = Arc::new(MangoClient::new_for_existing_account(
Client::new(
cluster,
commitment,
&owner,
Some(Duration::from_secs(cli.timeout)),
),
cli.mango_account,
owner,
)?);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let mango_client = Arc::new(
MangoClient::new_for_existing_account(
Client::new(
cluster,
commitment,
&owner,
Some(Duration::from_secs(cli.timeout)),
),
cli.mango_account,
owner,
)
.await?,
);
let debugging_handle = async {
let mut interval = time::interval(time::Duration::from_secs(5));
@ -120,17 +120,18 @@ fn main() -> Result<(), anyhow::Error> {
match cli.command {
Command::Crank { .. } => {
let client = mango_client.clone();
rt.block_on(crank::runner(
crank::runner(
client,
debugging_handle,
cli.interval_update_banks,
cli.interval_consume_events,
cli.interval_update_funding,
))
)
.await
}
Command::Taker { .. } => {
let client = mango_client.clone();
rt.block_on(taker::runner(client, debugging_handle))
taker::runner(client, debugging_handle).await
}
}
}

View File

@ -16,9 +16,8 @@ pub async fn runner(
mango_client: Arc<MangoClient>,
_debugging_handle: impl Future,
) -> Result<(), anyhow::Error> {
ensure_deposit(&mango_client)?;
ensure_oo(&mango_client)?;
ensure_deposit(&mango_client).await?;
ensure_oo(&mango_client).await?;
let mut price_arcs = HashMap::new();
for market_name in mango_client.context.serum3_market_indexes_by_name.keys() {
@ -30,6 +29,7 @@ pub async fn runner(
.first()
.unwrap(),
)
.await
.unwrap();
price_arcs.insert(
market_name.to_owned(),
@ -73,23 +73,25 @@ pub async fn runner(
Ok(())
}
fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let account = mango_client.mango_account()?;
async fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let account = mango_client.mango_account().await?;
for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() {
if account.serum3_orders(*market_index).is_err() {
mango_client.serum3_create_open_orders(serum3_market.market.name())?;
mango_client
.serum3_create_open_orders(serum3_market.market.name())
.await?;
}
}
Ok(())
}
fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let mango_account = mango_client.mango_account()?;
async fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let mango_account = mango_client.mango_account().await?;
for &token_index in mango_client.context.tokens.keys() {
let bank = mango_client.first_bank(token_index)?;
let bank = mango_client.first_bank(token_index).await?;
let desired_balance = I80F48::from_num(10_000 * 10u64.pow(bank.mint_decimals as u32));
let token_account_opt = mango_account.token_position(token_index).ok();
@ -114,7 +116,9 @@ fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error>
}
log::info!("Depositing {} {}", deposit_native, bank.name());
mango_client.token_deposit(bank.mint, desired_balance.to_num())?;
mango_client
.token_deposit(bank.mint, desired_balance.to_num())
.await?;
}
Ok(())
@ -126,33 +130,15 @@ pub async fn loop_blocking_price_update(
price: Arc<RwLock<I80F48>>,
) {
let mut interval = time::interval(Duration::from_secs(1));
let token_name = market_name.split('/').collect::<Vec<&str>>()[0];
loop {
interval.tick().await;
let client1 = mango_client.clone();
let market_name1 = market_name.clone();
let price = price.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let token_name = market_name1.split('/').collect::<Vec<&str>>()[0];
let fresh_price = client1.get_oracle_price(token_name).unwrap();
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
if let Ok(mut price) = price.write() {
*price = I80F48::from_num(fresh_price.price)
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
}
Ok(())
})
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
let fresh_price = mango_client.get_oracle_price(token_name).await.unwrap();
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
if let Ok(mut price) = price.write() {
*price = I80F48::from_num(fresh_price.price)
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
}
}
}
@ -165,7 +151,10 @@ pub async fn loop_blocking_orders(
let mut interval = time::interval(Duration::from_secs(5));
// Cancel existing orders
let orders: Vec<u128> = mango_client.serum3_cancel_all_orders(&market_name).unwrap();
let orders: Vec<u128> = mango_client
.serum3_cancel_all_orders(&market_name)
.await
.unwrap();
log::info!("Cancelled orders - {:?} for {}", orders, market_name);
loop {
@ -175,8 +164,8 @@ pub async fn loop_blocking_orders(
let market_name = market_name.clone();
let price = price.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
client.serum3_settle_funds(&market_name)?;
let res = (|| async move {
client.serum3_settle_funds(&market_name).await?;
let fresh_price = match price.read() {
Ok(price) => *price,
@ -188,16 +177,18 @@ pub async fn loop_blocking_orders(
let fresh_price = fresh_price.to_num::<f64>();
let bid_price = fresh_price + fresh_price * 0.1;
let res = client.serum3_place_order(
&market_name,
Serum3Side::Bid,
bid_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
);
let res = client
.serum3_place_order(
&market_name,
Serum3Side::Bid,
bid_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
)
.await;
if let Err(e) = res {
log::error!("Error while placing taker bid {:#?}", e)
} else {
@ -205,16 +196,18 @@ pub async fn loop_blocking_orders(
}
let ask_price = fresh_price - fresh_price * 0.1;
let res = client.serum3_place_order(
&market_name,
Serum3Side::Ask,
ask_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
);
let res = client
.serum3_place_order(
&market_name,
Serum3Side::Ask,
ask_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
)
.await;
if let Err(e) = res {
log::error!("Error while placing taker ask {:#?}", e)
} else {
@ -222,18 +215,11 @@ pub async fn loop_blocking_orders(
}
Ok(())
})
})()
.await;
match res {
Ok(inner_res) => {
if inner_res.is_err() {
log::error!("{}", inner_res.unwrap_err());
}
}
Err(join_error) => {
log::error!("{}", join_error);
}
if let Err(err) = res {
log::error!("{:?}", err);
}
}
}

View File

@ -1,14 +1,15 @@
use std::collections::HashSet;
use std::time::Duration;
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::{HealthCache, HealthType};
use mango_v4::state::{
Bank, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX,
Bank, MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX,
};
use solana_sdk::signature::Signature;
use itertools::Itertools;
use futures::{stream, StreamExt, TryStreamExt};
use rand::seq::SliceRandom;
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
@ -17,7 +18,7 @@ pub struct Config {
pub refresh_timeout: Duration,
}
pub fn jupiter_market_can_buy(
pub async fn jupiter_market_can_buy(
mango_client: &MangoClient,
token: TokenIndex,
quote_token: TokenIndex,
@ -41,10 +42,11 @@ pub fn jupiter_market_can_buy(
slippage,
client::JupiterSwapMode::ExactIn,
)
.await
.is_ok()
}
pub fn jupiter_market_can_sell(
pub async fn jupiter_market_can_sell(
mango_client: &MangoClient,
token: TokenIndex,
quote_token: TokenIndex,
@ -68,6 +70,7 @@ pub fn jupiter_market_can_sell(
slippage,
client::JupiterSwapMode::ExactOut,
)
.await
.is_ok()
}
@ -79,40 +82,50 @@ struct LiquidateHelper<'a> {
health_cache: &'a HealthCache,
maint_health: I80F48,
liqor_min_health_ratio: I80F48,
allowed_asset_tokens: HashSet<Pubkey>,
allowed_liab_tokens: HashSet<Pubkey>,
}
impl<'a> LiquidateHelper<'a> {
fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
async fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
// look for any open serum orders or settleable balances
let serum_force_cancels = self
.liqee
.active_serum3_orders()
.map(|orders| {
let serum_oos: anyhow::Result<Vec<_>> = stream::iter(self.liqee.active_serum3_orders())
.then(|orders| async {
let open_orders_account = self
.account_fetcher
.fetch_raw_account(&orders.open_orders)?;
.fetch_raw_account(&orders.open_orders)
.await?;
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
Ok((*orders, *open_orders))
})
.try_collect()
.await;
let serum_force_cancels = serum_oos?
.into_iter()
.filter_map(|(orders, open_orders)| {
let can_force_cancel = open_orders.native_coin_total > 0
|| open_orders.native_pc_total > 0
|| open_orders.referrer_rebates_accrued > 0;
if can_force_cancel {
Ok(Some(*orders))
Some(orders)
} else {
Ok(None)
None
}
})
.filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
.collect::<Vec<_>>();
if serum_force_cancels.is_empty() {
return Ok(None);
}
// Cancel all orders on a random serum market
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = self.client.serum3_liq_force_cancel_orders(
(self.pubkey, &self.liqee),
serum_orders.market_index,
&serum_orders.open_orders,
)?;
let sig = self
.client
.serum3_liq_force_cancel_orders(
(self.pubkey, &self.liqee),
serum_orders.market_index,
&serum_orders.open_orders,
)
.await?;
log::info!(
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
@ -123,7 +136,7 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
async fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
let perp_force_cancels = self
.liqee
.active_perp_positions()
@ -137,7 +150,8 @@ impl<'a> LiquidateHelper<'a> {
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = self
.client
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)?;
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)
.await?;
log::info!(
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
@ -148,11 +162,11 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
let mut perp_base_positions = self
.liqee
.active_perp_positions()
.map(|pp| {
async fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
let all_perp_base_positions: anyhow::Result<
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
> = stream::iter(self.liqee.active_perp_positions())
.then(|pp| async {
let base_lots = pp.base_position_lots();
if base_lots == 0 {
return Ok(None);
@ -160,7 +174,8 @@ impl<'a> LiquidateHelper<'a> {
let perp = self.client.context.perp(pp.market_index);
let oracle = self
.account_fetcher
.fetch_raw_account(&perp.market.oracle)?;
.fetch_raw_account(&perp.market.oracle)
.await?;
let price = perp.market.oracle_price(
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
None,
@ -172,8 +187,12 @@ impl<'a> LiquidateHelper<'a> {
I80F48::from(base_lots.abs()) * price,
)))
})
.filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
.try_collect()
.await;
let mut perp_base_positions = all_perp_base_positions?
.into_iter()
.filter_map(|x| x)
.collect::<Vec<_>>();
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
if perp_base_positions.is_empty() {
@ -194,10 +213,12 @@ impl<'a> LiquidateHelper<'a> {
let mut liqor = self
.account_fetcher
.fetch_fresh_mango_account(&self.client.mango_account_address)
.await
.context("getting liquidator account")?;
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
let health_cache =
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.await
.expect("always ok");
health_cache.max_perp_for_health_ratio(
*perp_market_index,
@ -208,11 +229,14 @@ impl<'a> LiquidateHelper<'a> {
};
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
let sig = self.client.perp_liq_base_position(
(self.pubkey, &self.liqee),
*perp_market_index,
side_signum * max_base_transfer_abs,
)?;
let sig = self
.client
.perp_liq_base_position(
(self.pubkey, &self.liqee),
*perp_market_index,
side_signum * max_base_transfer_abs,
)
.await?;
log::info!(
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
@ -223,7 +247,7 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
let perp_settle_health = self.health_cache.perp_settle_health();
let mut perp_settleable_pnl = self
.liqee
@ -262,7 +286,8 @@ impl<'a> LiquidateHelper<'a> {
perp_index,
direction,
2,
)?;
)
.await?;
if counters.is_empty() {
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
// then liquidation should continue, even though this step produced no transaction
@ -270,7 +295,9 @@ impl<'a> LiquidateHelper<'a> {
self.pubkey);
continue;
}
let (counter_key, counter_acc, _) = counters.first().unwrap();
let (counter_key, counter_acc, counter_pnl) = counters.first().unwrap();
log::info!("Trying to settle perp pnl account: {} market_index: {perp_index} amount: {pnl} against {counter_key} with pnl: {counter_pnl}", self.pubkey);
let (account_a, account_b) = if pnl > 0 {
((self.pubkey, self.liqee), (counter_key, counter_acc))
@ -279,7 +306,8 @@ impl<'a> LiquidateHelper<'a> {
};
let sig = self
.client
.perp_settle_pnl(perp_index, account_a, account_b)?;
.perp_settle_pnl(perp_index, account_a, account_b)
.await?;
log::info!(
"Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}",
self.pubkey,
@ -290,7 +318,7 @@ impl<'a> LiquidateHelper<'a> {
return Ok(None);
}
fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
async fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if self.health_cache.has_liquidatable_assets() {
return Ok(None);
}
@ -312,12 +340,15 @@ impl<'a> LiquidateHelper<'a> {
}
let (perp_market_index, _) = perp_bankruptcies.first().unwrap();
let sig = self.client.perp_liq_bankruptcy(
(self.pubkey, &self.liqee),
*perp_market_index,
// Always use the max amount, since the health effect is always positive
u64::MAX,
)?;
let sig = self
.client
.perp_liq_bankruptcy(
(self.pubkey, &self.liqee),
*perp_market_index,
// Always use the max amount, since the health effect is always positive
u64::MAX,
)
.await?;
log::info!(
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
@ -328,34 +359,36 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
let mut tokens = self
.liqee
.active_token_positions()
.map(|token_position| {
let token = self.client.context.token(token_position.token_index);
let bank = self
.account_fetcher
.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = self
.account_fetcher
.fetch_raw_account(&token.mint_info.oracle)?;
let price = bank.oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
None,
)?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
})
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
async fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
let tokens_maybe: anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> =
stream::iter(self.liqee.active_token_positions())
.then(|token_position| async {
let token = self.client.context.token(token_position.token_index);
let bank = self
.account_fetcher
.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = self
.account_fetcher
.fetch_raw_account(&token.mint_info.oracle)
.await?;
let price = bank.oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
None,
)?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
})
.try_collect()
.await;
let mut tokens = tokens_maybe?;
tokens.sort_by(|a, b| a.2.cmp(&b.2));
Ok(tokens)
}
fn max_token_liab_transfer(
async fn max_token_liab_transfer(
&self,
source: TokenIndex,
target: TokenIndex,
@ -363,6 +396,7 @@ impl<'a> LiquidateHelper<'a> {
let mut liqor = self
.account_fetcher
.fetch_fresh_mango_account(&self.client.mango_account_address)
.await
.context("getting liquidator account")?;
// Ensure the tokens are activated, so they appear in the health cache and
@ -371,10 +405,11 @@ impl<'a> LiquidateHelper<'a> {
liqor.ensure_token_position(target)?;
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.await
.expect("always ok");
let source_bank = self.client.first_bank(source)?;
let target_bank = self.client.first_bank(target)?;
let source_bank = self.client.first_bank(source).await?;
let target_bank = self.client.first_bank(target).await?;
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
let target_price = health_cache.token_info(target).unwrap().prices.oracle;
@ -394,19 +429,21 @@ impl<'a> LiquidateHelper<'a> {
Ok(amount)
}
fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() {
async fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.has_spot_assets() || !self.health_cache.has_spot_borrows() {
return Ok(None);
}
let tokens = self.tokens()?;
let tokens = self.tokens().await?;
let asset_token_index = tokens
.iter()
.rev()
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
asset_usdc_equivalent.is_positive()
&& jupiter_market_can_sell(self.client, *asset_token_index, QUOTE_TOKEN_INDEX)
&& self
.allowed_asset_tokens
.contains(&self.client.context.token(*asset_token_index).mint_info.mint)
})
.ok_or_else(|| {
anyhow::anyhow!(
@ -420,7 +457,9 @@ impl<'a> LiquidateHelper<'a> {
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
&& self
.allowed_liab_tokens
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
})
.ok_or_else(|| {
anyhow::anyhow!(
@ -433,6 +472,7 @@ impl<'a> LiquidateHelper<'a> {
let max_liab_transfer = self
.max_token_liab_transfer(liab_token_index, asset_token_index)
.await
.context("getting max_liab_transfer")?;
//
@ -447,6 +487,7 @@ impl<'a> LiquidateHelper<'a> {
liab_token_index,
max_liab_transfer,
)
.await
.context("sending liq_token_with_token")?;
log::info!(
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
@ -457,12 +498,12 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.can_call_spot_bankruptcy() {
return Ok(None);
}
let tokens = self.tokens()?;
let tokens = self.tokens().await?;
if tokens.is_empty() {
anyhow::bail!(
@ -474,7 +515,9 @@ impl<'a> LiquidateHelper<'a> {
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
&& self
.allowed_liab_tokens
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
})
.ok_or_else(|| {
anyhow::anyhow!(
@ -486,8 +529,9 @@ impl<'a> LiquidateHelper<'a> {
.0;
let quote_token_index = 0;
let max_liab_transfer =
self.max_token_liab_transfer(liab_token_index, quote_token_index)?;
let max_liab_transfer = self
.max_token_liab_transfer(liab_token_index, quote_token_index)
.await?;
let sig = self
.client
@ -496,6 +540,7 @@ impl<'a> LiquidateHelper<'a> {
liab_token_index,
max_liab_transfer,
)
.await
.context("sending liq_token_bankruptcy")?;
log::info!(
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
@ -506,7 +551,7 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
fn send_liq_tx(&self) -> anyhow::Result<Signature> {
async fn send_liq_tx(&self) -> anyhow::Result<Signature> {
// TODO: Should we make an attempt to settle positive PNL first?
// The problem with it is that small market movements can continuously create
// small amounts of new positive PNL while base_position > 0.
@ -517,14 +562,14 @@ impl<'a> LiquidateHelper<'a> {
// }
// Try to close orders before touching the user's positions
if let Some(txsig) = self.perp_close_orders()? {
if let Some(txsig) = self.perp_close_orders().await? {
return Ok(txsig);
}
if let Some(txsig) = self.serum3_close_orders()? {
if let Some(txsig) = self.serum3_close_orders().await? {
return Ok(txsig);
}
if let Some(txsig) = self.perp_liq_base_position()? {
if let Some(txsig) = self.perp_liq_base_position().await? {
return Ok(txsig);
}
@ -533,21 +578,21 @@ impl<'a> LiquidateHelper<'a> {
// It's possible that some positive pnl can't be settled (if there's
// no liquid counterparty) and that some negative pnl can't be settled
// (if the liqee isn't liquid enough).
if let Some(txsig) = self.perp_settle_pnl()? {
if let Some(txsig) = self.perp_settle_pnl().await? {
return Ok(txsig);
}
if let Some(txsig) = self.token_liq()? {
if let Some(txsig) = self.token_liq().await? {
return Ok(txsig);
}
// Socialize/insurance fund unsettleable negative pnl
if let Some(txsig) = self.perp_liq_bankruptcy()? {
if let Some(txsig) = self.perp_liq_bankruptcy().await? {
return Ok(txsig);
}
// Socialize/insurance fund unliquidatable borrows
if let Some(txsig) = self.token_liq_bankruptcy()? {
if let Some(txsig) = self.token_liq_bankruptcy().await? {
return Ok(txsig);
}
@ -562,7 +607,7 @@ impl<'a> LiquidateHelper<'a> {
}
#[allow(clippy::too_many_arguments)]
pub fn maybe_liquidate_account(
pub async fn maybe_liquidate_account(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
pubkey: &Pubkey,
@ -571,8 +616,9 @@ pub fn maybe_liquidate_account(
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
let account = account_fetcher.fetch_mango_account(pubkey)?;
let health_cache =
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
.await
.expect("always ok");
let maint_health = health_cache.health(HealthType::Maint);
if !health_cache.is_liquidatable() {
return Ok(false);
@ -588,15 +634,24 @@ pub fn maybe_liquidate_account(
// Fetch a fresh account and re-compute
// This is -- unfortunately -- needed because the websocket streams seem to not
// be great at providing timely updates to the account data.
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
let health_cache =
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
.await
.expect("always ok");
if !health_cache.is_liquidatable() {
return Ok(false);
}
let maint_health = health_cache.health(HealthType::Maint);
let all_token_mints = HashSet::from_iter(
mango_client
.context
.tokens
.values()
.map(|c| c.mint_info.mint),
);
// try liquidating
let txsig = LiquidateHelper {
client: mango_client,
@ -606,15 +661,21 @@ pub fn maybe_liquidate_account(
health_cache: &health_cache,
maint_health,
liqor_min_health_ratio,
allowed_asset_tokens: all_token_mints.clone(),
allowed_liab_tokens: all_token_mints,
}
.send_liq_tx()?;
.send_liq_tx()
.await?;
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
) {
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
if let Err(e) = account_fetcher
.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
)
.await
{
log::info!("could not refresh after liquidation: {}", e);
}
@ -622,14 +683,14 @@ pub fn maybe_liquidate_account(
}
#[allow(clippy::too_many_arguments)]
pub fn maybe_liquidate_one<'a>(
pub async fn maybe_liquidate_one<'a>(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
accounts: impl Iterator<Item = &'a Pubkey>,
config: &Config,
) -> bool {
for pubkey in accounts {
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config).await {
Err(err) => {
// Not all errors need to be raised to the user's attention.
let mut log_level = log::Level::Error;

View File

@ -114,13 +114,15 @@ async fn main() -> anyhow::Result<()> {
// Reading accounts from chain_data
let account_fetcher = Arc::new(chain_data::AccountFetcher {
chain_data: chain_data.clone(),
rpc: client.rpc(),
rpc: client.rpc_async(),
});
let mango_account = account_fetcher.fetch_fresh_mango_account(&cli.liqor_mango_account)?;
let mango_account = account_fetcher
.fetch_fresh_mango_account(&cli.liqor_mango_account)
.await?;
let mango_group = mango_account.fixed.group;
let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?;
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
let mango_oracles = group_context
.tokens
@ -258,7 +260,7 @@ async fn main() -> anyhow::Result<()> {
std::iter::once(&account_write.pubkey),
&liq_config,
&rebalance_config,
)?;
).await?;
}
if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) {
@ -280,7 +282,7 @@ async fn main() -> anyhow::Result<()> {
mango_accounts.iter(),
&liq_config,
&rebalance_config,
)?;
).await?;
}
}
},
@ -313,12 +315,12 @@ async fn main() -> anyhow::Result<()> {
mango_accounts.iter(),
&liq_config,
&rebalance_config,
)?;
).await?;
},
_ = rebalance_interval.tick() => {
if one_snapshot_done {
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config) {
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config).await {
log::error!("failed to rebalance liqor: {:?}", err);
// Workaround: We really need a sequence enforcer in the liquidator since we don't want to
@ -332,20 +334,20 @@ async fn main() -> anyhow::Result<()> {
}
}
fn liquidate<'a>(
async fn liquidate<'a>(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
accounts: impl Iterator<Item = &'a Pubkey>,
config: &liquidate::Config,
rebalance_config: &rebalance::Config,
) -> anyhow::Result<()> {
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) {
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config).await {
return Ok(());
}
let liqor = &mango_client.mango_account_address;
if let Err(err) =
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config)
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config).await
{
log::error!("failed to rebalance liqor: {:?}", err);
}

View File

@ -6,6 +6,7 @@ use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
use futures::{stream, StreamExt, TryStreamExt};
use std::{collections::HashMap, time::Duration};
pub struct Config {
@ -20,14 +21,14 @@ struct TokenState {
}
impl TokenState {
fn new_position(
async fn new_position(
token: &TokenContext,
position: &TokenPosition,
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<Self> {
let bank = Self::bank(token, account_fetcher)?;
Ok(Self {
price: Self::fetch_price(token, &bank, account_fetcher)?,
price: Self::fetch_price(token, &bank, account_fetcher).await?,
native_position: position.native(&bank),
})
}
@ -39,12 +40,14 @@ impl TokenState {
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
}
fn fetch_price(
async fn fetch_price(
token: &TokenContext,
bank: &Bank,
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<I80F48> {
let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?;
let oracle = account_fetcher
.fetch_raw_account(&token.mint_info.oracle)
.await?;
bank.oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
None,
@ -54,7 +57,7 @@ impl TokenState {
}
#[allow(clippy::too_many_arguments)]
pub fn zero_all_non_quote(
pub async fn zero_all_non_quote(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
mango_account_address: &Pubkey,
@ -67,27 +70,32 @@ pub fn zero_all_non_quote(
let account = account_fetcher.fetch_mango_account(mango_account_address)?;
let tokens = account
.active_token_positions()
.map(|token_position| {
let token = mango_client.context.token(token_position.token_index);
Ok((
token.token_index,
TokenState::new_position(token, token_position, account_fetcher)?,
))
})
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
let tokens: anyhow::Result<HashMap<TokenIndex, TokenState>> =
stream::iter(account.active_token_positions())
.then(|token_position| async {
let token = mango_client.context.token(token_position.token_index);
Ok((
token.token_index,
TokenState::new_position(token, token_position, account_fetcher).await?,
))
})
.try_collect()
.await;
let tokens = tokens?;
log::trace!("account tokens: {:?}", tokens);
// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
let refresh_mango_account =
|account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result<bool> {
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*mango_account_address],
max_slot,
config.refresh_timeout,
) {
let refresh_mango_account = |txsig| async move {
let res: anyhow::Result<bool> = {
let max_slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
if let Err(e) = account_fetcher
.refresh_accounts_via_rpc_until_slot(
&[*mango_account_address],
max_slot,
config.refresh_timeout,
)
.await
{
// If we don't get fresh data, maybe the tx landed on a fork?
// Rebalance is technically still ok.
log::info!("could not refresh account data: {}", e);
@ -95,6 +103,8 @@ pub fn zero_all_non_quote(
}
Ok(true)
};
res
};
for (token_index, token_state) in tokens {
let token = mango_client.context.token(token_index);
@ -120,13 +130,15 @@ pub fn zero_all_non_quote(
if amount > dust_threshold {
// Sell
let txsig = mango_client.jupiter_swap(
token_mint,
quote_mint,
amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactIn,
)?;
let txsig = mango_client
.jupiter_swap(
token_mint,
quote_mint,
amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactIn,
)
.await?;
log::info!(
"sold {} {} for {} in tx {}",
token.native_to_ui(amount),
@ -134,12 +146,13 @@ pub fn zero_all_non_quote(
quote_token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
if !refresh_mango_account(txsig).await? {
return Ok(());
}
let bank = TokenState::bank(token, account_fetcher)?;
amount = mango_client
.mango_account()?
.mango_account()
.await?
.token_position_and_raw_index(token_index)
.map(|(position, _)| position.native(&bank))
.unwrap_or(I80F48::ZERO);
@ -147,13 +160,15 @@ pub fn zero_all_non_quote(
// Buy
let buy_amount = (-token_state.native_position).ceil()
+ (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
let txsig = mango_client.jupiter_swap(
quote_mint,
token_mint,
buy_amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactOut,
)?;
let txsig = mango_client
.jupiter_swap(
quote_mint,
token_mint,
buy_amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactOut,
)
.await?;
log::info!(
"bought {} {} for {} in tx {}",
token.native_to_ui(buy_amount),
@ -161,12 +176,13 @@ pub fn zero_all_non_quote(
quote_token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
if !refresh_mango_account(txsig).await? {
return Ok(());
}
let bank = TokenState::bank(token, account_fetcher)?;
amount = mango_client
.mango_account()?
.mango_account()
.await?
.token_position_and_raw_index(token_index)
.map(|(position, _)| position.native(&bank))
.unwrap_or(I80F48::ZERO);
@ -176,15 +192,16 @@ pub fn zero_all_non_quote(
// TokenPosition is freed up
if amount > 0 && amount <= dust_threshold {
let allow_borrow = false;
let txsig =
mango_client.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)?;
let txsig = mango_client
.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)
.await?;
log::info!(
"withdrew {} {} to liqor wallet in {}",
token.native_to_ui(amount),
token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
if !refresh_mango_account(txsig).await? {
return Ok(());
}
} else if amount > dust_threshold {

View File

@ -175,7 +175,10 @@ pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::
loop {
info!("connecting to solana websocket streams");
let out = feed_data(&config, mango_oracles.clone(), sender.clone());
let _ = out.await;
let result = out.await;
if let Err(err) = result {
warn!("websocket stream error: {err}");
}
}
});
}

View File

@ -19,7 +19,7 @@
"build": "npm run build:esm; npm run build:cjs",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 15000",
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 300000",
"clean": "rm -rf dist",
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",

View File

@ -1,7 +1,6 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use crate::error::*;
use crate::state::{
@ -12,8 +11,6 @@ use crate::util::checked_math as cm;
use super::*;
const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001);
/// Information about prices for a bank or perp market.
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct Prices {
@ -201,7 +198,7 @@ impl Serum3Info {
}
#[derive(Clone)]
struct Serum3Reserved {
pub(crate) struct Serum3Reserved {
/// base tokens when the serum3info.reserved_quote get converted to base and added to reserved_base
all_reserved_as_base: I80F48,
/// ditto the other way around
@ -442,11 +439,15 @@ impl HealthCache {
Ok(())
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self.token_infos.iter().any(|ti| {
pub fn has_spot_assets(&self) -> bool {
self.token_infos.iter().any(|ti| {
// can use token_liq_with_token
ti.balance_native.is_positive()
});
})
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self.has_spot_assets();
// can use serum3_liq_force_cancel_orders
let serum3_cancelable = self
.serum3_infos
@ -455,10 +456,11 @@ impl HealthCache {
let perp_liquidatable = self.perp_infos.iter().any(|p| {
// can use perp_liq_base_position
p.base_lots != 0
// can use perp_settle_pnl
|| p.quote > ONE_NATIVE_USDC_IN_USD // TODO: we're not guaranteed to be able to settle positive perp pnl!
// can use perp_liq_force_cancel_orders
|| p.has_open_orders
// A remaining quote position can be reduced with perp_settle_pnl and that can improve health.
// However, since it's not guaranteed that there is a counterparty, a positive perp quote position
// does not prevent bankruptcy.
});
spot_liquidatable || serum3_cancelable || perp_liquidatable
}
@ -477,7 +479,7 @@ impl HealthCache {
self.has_spot_borrows() || perp_borrows
}
fn compute_serum3_reservations(
pub(crate) fn compute_serum3_reservations(
&self,
health_type: HealthType,
) -> (Vec<I80F48>, Vec<Serum3Reserved>) {

View File

@ -79,6 +79,46 @@ impl HealthCache {
Ok(resulting_cache)
}
pub fn max_swap_source_for_health_ratio(
&self,
account: &MangoAccountValue,
source_bank: &Bank,
source_oracle_price: I80F48,
target_bank: &Bank,
price: I80F48,
min_ratio: I80F48,
) -> Result<I80F48> {
self.max_swap_source_for_health_fn(
account,
source_bank,
source_oracle_price,
target_bank,
price,
min_ratio,
|cache| cache.health_ratio(HealthType::Init),
)
}
pub fn max_swap_source_for_health(
&self,
account: &MangoAccountValue,
source_bank: &Bank,
source_oracle_price: I80F48,
target_bank: &Bank,
price: I80F48,
min_ratio: I80F48,
) -> Result<I80F48> {
self.max_swap_source_for_health_fn(
account,
source_bank,
source_oracle_price,
target_bank,
price,
min_ratio,
|cache| cache.health(HealthType::Init),
)
}
/// How many source native tokens may be swapped for target tokens while staying
/// above the min_ratio health ratio.
///
@ -90,29 +130,27 @@ impl HealthCache {
/// Positions for the source and deposit token index must already exist in the account.
///
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
pub fn max_swap_source_for_health_ratio(
pub fn max_swap_source_for_health_fn(
&self,
account: &MangoAccountValue,
source_bank: &Bank,
source_oracle_price: I80F48,
target_bank: &Bank,
price: I80F48,
min_ratio: I80F48,
min_fn_value: I80F48,
target_fn: fn(&HealthCache) -> I80F48,
) -> Result<I80F48> {
// The health_ratio is nonlinear based on swap amount.
// The health and health_ratio are nonlinear based on swap amount.
// For large swap amounts the slope is guaranteed to be negative (unless the price
// is extremely good), but small amounts can have positive slope (e.g. using
// source deposits to pay back target borrows).
//
// That means:
// - even if the initial ratio is < min_ratio it can be useful to swap to *increase* health
// - be careful about finding the min_ratio point: the function isn't convex
// - even if the initial value is < min_fn_value it can be useful to swap to *increase* health
// - even if initial value is < 0, swapping can increase health (maybe above 0)
// - be careful about finding the min_fn_value: the function isn't convex
let health_type = HealthType::Init;
let initial_ratio = self.health_ratio(health_type);
if initial_ratio < 0 {
return Ok(I80F48::ZERO);
}
// Fail if the health cache (or consequently the account) don't have existing
// positions for the source and target token index.
@ -122,6 +160,10 @@ impl HealthCache {
let source = &self.token_infos[source_index];
let target = &self.token_infos[target_index];
let (tokens_max_reserved, _) = self.compute_serum3_reservations(health_type);
let source_reserved = tokens_max_reserved[source_index];
let target_reserved = tokens_max_reserved[target_index];
// If the price is sufficiently good, then health will just increase from swapping:
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
// increases it by x * target_asset_weight * price_factor.
@ -157,106 +199,81 @@ impl HealthCache {
Err(err) => Err(err),
}
};
let health_ratio_after_swap = |amount| {
let fn_value_after_swap = |amount| {
Ok(cache_after_swap(amount)?
.map(|c| c.health_ratio(HealthType::Init))
.as_ref()
.map(target_fn)
.unwrap_or(I80F48::MIN))
};
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
// The function we're looking at has a unique maximum.
//
// If we discount serum3 reservations, there are two key slope changes:
// Assume source.balance > 0 and target.balance < 0.
// When these values flip sign, the health slope decreases, but could still be positive.
// After point1 it's definitely negative (due to final_health_slope check above).
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
let source_for_zero_target_balance = -target.balance_native / price;
let point0_amount = source
.balance_native
.min(source_for_zero_target_balance)
.max(I80F48::ZERO);
let point1_amount = source
.balance_native
.max(source_for_zero_target_balance)
.max(I80F48::ZERO);
let point0_ratio = health_ratio_after_swap(point0_amount)?;
let (point1_ratio, point1_health) = {
let cache = cache_after_swap(point1_amount)?;
cache
.map(|c| (c.health_ratio(HealthType::Init), c.health(HealthType::Init)))
.unwrap_or((I80F48::MIN, I80F48::MIN))
//
// The first thing we do is to find this maximum.
let (amount_for_max_value, max_value) = {
// The largest amount that the maximum could be at
let rightmost = (source.balance_native.abs() + source_reserved)
.max((target.balance_native.abs() + target_reserved) / price);
find_maximum(
I80F48::ZERO,
rightmost,
I80F48::from_num(0.1),
fn_value_after_swap,
)?
};
assert!(amount_for_max_value >= 0);
let amount =
if initial_ratio <= min_ratio && point0_ratio < min_ratio && point1_ratio < min_ratio {
// If we have to stay below the target ratio, pick the highest one
if point0_ratio > initial_ratio {
if point1_ratio > point0_ratio {
point1_amount
} else {
point0_amount
}
} else if point1_ratio > initial_ratio {
point1_amount
} else {
I80F48::ZERO
}
} else if point1_ratio >= min_ratio {
// If point1_ratio is still bigger than min_ratio, the target amount must be >point1_amount
// search to the right of point1_amount: but how far?
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
// zero health: health
// - source_liab_weight * source_liab_price * a
// + target_asset_weight * target_asset_price * price * a = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
if point1_health <= 0 {
return Ok(I80F48::ZERO);
}
let zero_health_estimate = point1_amount - point1_health / final_health_slope;
let right_bound = scan_right_until_less_than(
zero_health_estimate,
min_ratio,
health_ratio_after_swap,
)?;
if right_bound == zero_health_estimate {
binary_search(
point1_amount,
point1_ratio,
right_bound,
min_ratio,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
} else {
binary_search(
zero_health_estimate,
health_ratio_after_swap(zero_health_estimate)?,
right_bound,
min_ratio,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
}
} else if point0_ratio >= min_ratio {
// Must be between point0_amount and point1_amount.
if max_value <= min_fn_value {
// We cannot reach min_ratio, just return the max
return Ok(amount_for_max_value);
}
let amount = {
// Now max_value is bigger than min_fn_value, the target amount must be >amount_for_max_value.
// Search to the right of amount_for_max_value: but how far?
// Use a simple estimation for the amount that would lead to zero health:
// health
// - source_liab_weight * source_liab_price * a
// + target_asset_weight * target_asset_price * price * a = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
let health_at_max_value = cache_after_swap(amount_for_max_value)?
.map(|c| c.health(health_type))
.unwrap_or(I80F48::MIN);
if health_at_max_value <= 0 {
return Ok(I80F48::ZERO);
}
let zero_health_estimate =
amount_for_max_value - health_at_max_value / final_health_slope;
let right_bound = scan_right_until_less_than(
zero_health_estimate,
min_fn_value,
fn_value_after_swap,
)?;
if right_bound == zero_health_estimate {
binary_search(
point0_amount,
point0_ratio,
point1_amount,
min_ratio,
amount_for_max_value,
max_value,
right_bound,
min_fn_value,
I80F48::from_num(0.1),
health_ratio_after_swap,
fn_value_after_swap,
)?
} else {
// Must be between 0 and point0_amount
binary_search(
I80F48::ZERO,
initial_ratio,
point0_amount,
min_ratio,
zero_health_estimate,
fn_value_after_swap(zero_health_estimate)?,
right_bound,
min_fn_value,
I80F48::from_num(0.1),
health_ratio_after_swap,
fn_value_after_swap,
)?
};
}
};
assert!(amount >= 0);
Ok(amount)
@ -449,6 +466,77 @@ fn binary_search(
Err(error_msg!("binary search iterations exhausted"))
}
/// This is not a generic function. It assumes there is a unique maximum between left and right.
fn find_maximum(
mut left: I80F48,
mut right: I80F48,
min_step: I80F48,
fun: impl Fn(I80F48) -> Result<I80F48>,
) -> Result<(I80F48, I80F48)> {
assert!(right >= left);
let half = I80F48::from_num(0.5);
let mut mid = half * (left + right);
let mut left_value = fun(left)?;
let mut right_value = fun(right)?;
let mut mid_value = fun(mid)?;
while (right - left) > min_step {
if left_value >= mid_value {
// max must be between left and mid
assert!(mid_value >= right_value);
right = mid;
right_value = mid_value;
mid = half * (left + mid);
mid_value = fun(mid)?
} else if mid_value <= right_value {
// max must be between mid and right
assert!(left_value <= mid_value);
left = mid;
left_value = mid_value;
mid = half * (mid + right);
mid_value = fun(mid)?;
} else {
// mid is larger than both left and right, max could be on either side
let leftmid = half * (left + mid);
let leftmid_value = fun(leftmid)?;
assert!(leftmid_value >= left_value);
if leftmid_value >= mid_value {
// max between left and mid
right = mid;
right_value = mid_value;
mid = leftmid;
mid_value = leftmid_value;
continue;
}
let rightmid = half * (mid + right);
let rightmid_value = fun(rightmid)?;
assert!(rightmid_value >= right_value);
if rightmid_value >= mid_value {
// max between mid and right
left = mid;
left_value = mid_value;
mid = rightmid;
mid_value = rightmid_value;
continue;
}
// max between leftmid and rightmid
left = leftmid;
left_value = leftmid_value;
right = rightmid;
right_value = rightmid_value;
}
}
if left_value >= mid_value {
Ok((left, left_value))
} else if mid_value >= right_value {
Ok((mid, mid_value))
} else {
Ok((right, right_value))
}
}
#[cfg(test)]
mod tests {
use super::super::test::*;
@ -532,6 +620,8 @@ mod tests {
I80F48::ZERO
);
type MaxSwapFn = fn(&HealthCache) -> I80F48;
let adjust_by_usdc = |c: &mut HealthCache, ti: TokenIndex, usdc: f64| {
let ti = &mut c.token_infos[ti as usize];
ti.balance_native += I80F48::from_num(usdc) / ti.prices.oracle;
@ -539,9 +629,10 @@ mod tests {
let find_max_swap_actual = |c: &HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: f64,
min_value: f64,
price_factor: f64,
banks: [Bank; 3]| {
banks: [Bank; 3],
max_swap_fn: MaxSwapFn| {
let source_price = &c.token_infos[source as usize].prices;
let source_bank = &banks[source as usize];
let target_price = &c.token_infos[target as usize].prices;
@ -549,19 +640,20 @@ mod tests {
let swap_price =
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
let source_amount = c
.max_swap_source_for_health_ratio(
.max_swap_source_for_health_fn(
&account,
source_bank,
source_price.oracle,
target_bank,
swap_price,
I80F48::from_num(ratio),
I80F48::from_num(min_value),
max_swap_fn,
)
.unwrap();
if source_amount == I80F48::MAX {
return (f64::MAX, f64::MAX, f64::MAX, f64::MAX);
}
let ratio_for_amount = |amount| {
let value_for_amount = |amount| {
c.cache_after_swap(
&account,
source_bank,
@ -570,152 +662,262 @@ mod tests {
I80F48::from(amount),
swap_price,
)
.map(|c| c.health_ratio(HealthType::Init).to_num::<f64>())
.map(|c| max_swap_fn(&c).to_num::<f64>())
.unwrap_or(f64::MIN)
};
(
source_amount.to_num(),
ratio_for_amount(source_amount),
ratio_for_amount(source_amount - I80F48::ONE),
ratio_for_amount(source_amount + I80F48::ONE),
value_for_amount(source_amount),
value_for_amount(source_amount - I80F48::ONE),
value_for_amount(source_amount + I80F48::ONE),
)
};
let check_max_swap_result = |c: &HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: f64,
min_value: f64,
price_factor: f64,
banks: [Bank; 3]| {
let (source_amount, actual_ratio, minus_ratio, plus_ratio) =
find_max_swap_actual(c, source, target, ratio, price_factor, banks);
banks: [Bank; 3],
max_swap_fn: MaxSwapFn| {
let (source_amount, actual_value, minus_value, plus_value) = find_max_swap_actual(
c,
source,
target,
min_value,
price_factor,
banks,
max_swap_fn,
);
println!(
"checking {source} to {target} for price_factor: {price_factor}, target ratio {ratio}: actual ratios: {minus_ratio}/{actual_ratio}/{plus_ratio}, amount: {source_amount}",
"checking {source} to {target} for price_factor: {price_factor}, target {min_value}: actual: {minus_value}/{actual_value}/{plus_value}, amount: {source_amount}",
);
assert!(actual_ratio >= ratio);
// either we're within tolerance of the target, or swapping 1 more would
// bring us below the target
assert!(actual_ratio < ratio + 1.0 || plus_ratio < ratio);
if actual_value < min_value {
// check that swapping more would decrease the ratio!
assert!(plus_value < actual_value);
} else {
assert!(actual_value >= min_value);
// either we're within tolerance of the target, or swapping 1 more would
// bring us below the target
assert!(actual_value < min_value + 1.0 || plus_value < min_value);
}
};
{
println!("test 0");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 1, 100.0);
let health_fn: Box<MaxSwapFn> = Box::new(|c: &HealthCache| c.health(HealthType::Init));
let health_ratio_fn: Box<MaxSwapFn> =
Box::new(|c: &HealthCache| c.health_ratio(HealthType::Init));
for price_factor in [0.1, 0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
for (test_name, max_swap_fn) in [("health", health_fn), ("health_ratio", health_ratio_fn)] {
let check = |c: &HealthCache,
source: TokenIndex,
target: TokenIndex,
min_value: f64,
price_factor: f64,
banks: [Bank; 3]| {
check_max_swap_result(
c,
source,
target,
min_value,
price_factor,
banks,
*max_swap_fn,
)
};
let find_max_swap = |c: &HealthCache,
source: TokenIndex,
target: TokenIndex,
min_value: f64,
price_factor: f64,
banks: [Bank; 3]| {
find_max_swap_actual(
c,
source,
target,
min_value,
price_factor,
banks,
*max_swap_fn,
)
};
{
println!("test 0 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 1, 100.0);
for price_factor in [0.1, 0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 0, 1, target, price_factor, banks);
check(&health_cache, 1, 0, target, price_factor, banks);
check(&health_cache, 0, 2, target, price_factor, banks);
}
}
// At this unlikely price it's healthy to swap infinitely
assert_eq!(
find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0,
f64::MAX
);
}
{
println!("test 1 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
for price_factor in [0.1, 0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 0, 1, target, price_factor, banks);
check(&health_cache, 1, 0, target, price_factor, banks);
check(&health_cache, 0, 2, target, price_factor, banks);
check(&health_cache, 2, 0, target, price_factor, banks);
}
}
}
// At this unlikely price it's healthy to swap infinitely
assert_eq!(
find_max_swap_actual(&health_cache, 0, 1, 50.0, 1.5, banks).0,
f64::MAX
);
}
{
println!("test 2 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -50.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
// possible even though the init ratio is <100
check(&health_cache, 1, 0, 100.0, 1.0, banks);
}
{
println!("test 1");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
{
println!("test 3 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -30.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
adjust_by_usdc(&mut health_cache, 2, -30.0);
for price_factor in [0.1, 0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
check_max_swap_result(&health_cache, 2, 0, target, price_factor, banks);
// swapping with a high ratio advises paying back all liabs
// and then swapping even more because increasing assets in 0 has better asset weight
let init_ratio = health_cache.health_ratio(HealthType::Init);
let (amount, actual_ratio, _, _) =
find_max_swap(&health_cache, 1, 0, 100.0, 1.0, banks);
println!(
"init {}, after {}, amount {}",
init_ratio, actual_ratio, amount
);
assert!(actual_ratio / 2.0 > init_ratio);
assert!((amount as f64 - 100.0 / 3.0).abs() < 1.0);
}
{
println!("test 4 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, 100.0);
adjust_by_usdc(&mut health_cache, 1, -2.0);
adjust_by_usdc(&mut health_cache, 2, -65.0);
let init_ratio = health_cache.health_ratio(HealthType::Init);
assert!(init_ratio > 3 && init_ratio < 4);
check(&health_cache, 0, 1, 1.0, 1.0, banks);
check(&health_cache, 0, 1, 3.0, 1.0, banks);
check(&health_cache, 0, 1, 4.0, 1.0, banks);
}
{
// check with net borrow limits
println!("test 5 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 1, 100.0);
let mut banks = banks.clone();
banks[0].net_borrow_limit_per_window_quote = 50;
// The net borrow limit restricts the amount that can be swapped
// (tracking happens without decimals)
assert!(find_max_swap(&health_cache, 0, 1, 1.0, 1.0, banks).0 < 51.0);
}
{
// check with serum reserved
println!("test 6 {test_name}");
let mut health_cache = health_cache.clone();
health_cache.serum3_infos = vec![Serum3Info {
base_index: 1,
quote_index: 0,
market_index: 0,
reserved_base: I80F48::from(30 / 3),
reserved_quote: I80F48::from(30 / 2),
}];
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, -40.0);
adjust_by_usdc(&mut health_cache, 2, 120.0);
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 0, 1, target, price_factor, banks);
check(&health_cache, 1, 0, target, price_factor, banks);
check(&health_cache, 0, 2, target, price_factor, banks);
check(&health_cache, 1, 2, target, price_factor, banks);
check(&health_cache, 2, 0, target, price_factor, banks);
check(&health_cache, 2, 1, target, price_factor, banks);
}
}
}
}
{
println!("test 2");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -50.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
// possible even though the init ratio is <100
check_max_swap_result(&health_cache, 1, 0, 100.0, 1.0, banks);
}
{
// check starting with negative health but swapping can make it positive
println!("test 7 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, 20.0);
assert!(health_cache.health(HealthType::Init) < 0);
{
println!("test 3");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -30.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
adjust_by_usdc(&mut health_cache, 2, -30.0);
if test_name == "health" {
assert!(find_max_swap(&health_cache, 1, 0, 1.0, 1.0, banks).0 > 0.0);
}
// swapping with a high ratio advises paying back all liabs
// and then swapping even more because increasing assets in 0 has better asset weight
let init_ratio = health_cache.health_ratio(HealthType::Init);
let (amount, actual_ratio, _, _) =
find_max_swap_actual(&health_cache, 1, 0, 100.0, 1.0, banks);
println!(
"init {}, after {}, amount {}",
init_ratio, actual_ratio, amount
);
assert!(actual_ratio / 2.0 > init_ratio);
assert!((amount as f64 - 100.0 / 3.0).abs() < 1.0);
}
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 1, 0, target, price_factor, banks);
}
}
}
{
println!("test 4");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, 100.0);
adjust_by_usdc(&mut health_cache, 1, -2.0);
adjust_by_usdc(&mut health_cache, 2, -65.0);
{
// check starting with negative health but swapping can't make it positive
println!("test 8 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, 10.0);
assert!(health_cache.health(HealthType::Init) < 0);
let init_ratio = health_cache.health_ratio(HealthType::Init);
assert!(init_ratio > 3 && init_ratio < 4);
if test_name == "health" {
assert!(find_max_swap(&health_cache, 1, 0, 1.0, 1.0, banks).0 > 0.0);
}
check_max_swap_result(&health_cache, 0, 1, 1.0, 1.0, banks);
check_max_swap_result(&health_cache, 0, 1, 3.0, 1.0, banks);
check_max_swap_result(&health_cache, 0, 1, 4.0, 1.0, banks);
}
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 1, 0, target, price_factor, banks);
}
}
}
{
// check with net borrow limits
println!("test 5");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 1, 100.0);
let mut banks = banks.clone();
banks[0].net_borrow_limit_per_window_quote = 50;
{
// swap some assets into a zero-asset-weight token
println!("test 9 {test_name}");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 0, 10.0);
health_cache.token_infos[1].init_asset_weight = I80F48::from(0);
// The net borrow limit restricts the amount that can be swapped
// (tracking happens without decimals)
assert!(find_max_swap_actual(&health_cache, 0, 1, 1.0, 1.0, banks).0 < 51.0);
}
assert!(find_max_swap(&health_cache, 0, 1, 1.0, 1.0, banks).0 > 0.0);
{
// check with serum reserved
println!("test 6");
let mut health_cache = health_cache.clone();
health_cache.serum3_infos = vec![Serum3Info {
base_index: 1,
quote_index: 0,
market_index: 0,
reserved_base: I80F48::from(30 / 3),
reserved_quote: I80F48::from(30 / 2),
}];
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, -40.0);
adjust_by_usdc(&mut health_cache, 2, 120.0);
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 2, target, price_factor, banks);
check_max_swap_result(&health_cache, 2, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 2, 1, target, price_factor, banks);
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 0, 1, target, price_factor, banks);
}
}
}
}

View File

@ -24,13 +24,14 @@ pub struct AccountClose<'info> {
pub token_program: Program<'info, Token>,
}
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
let group = ctx.accounts.group.load()?;
pub fn account_close(ctx: Context<AccountClose>, force_close: bool) -> Result<()> {
let account = ctx.accounts.account.load_mut()?;
// don't perform checks if group is just testing
if !group.is_testing() {
if !ctx.accounts.group.load()?.is_testing() {
require!(!force_close, MangoError::SomeError);
}
if !force_close {
require!(!account.fixed.being_liquidated(), MangoError::SomeError);
for ele in account.all_token_positions() {
require_eq!(ele.is_active(), false);

View File

@ -387,6 +387,15 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
approved_amount
};
// Enforce min vault to deposits ratio
if loan > 0 {
let vault_ai = vaults
.iter()
.find(|vault_ai| vault_ai.key == &bank.vault)
.unwrap();
bank.enforce_min_vault_to_deposits_ratio(vault_ai)?;
}
let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate);
cm!(bank.collected_fees_native += loan_origination_fee);

View File

@ -344,8 +344,19 @@ pub fn serum3_place_order(
// Placing an order cannot increase vault balance
require_gte!(before_vault, after_vault);
// Charge the difference in vault balance to the user's account
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
// Enforce min vault to deposits ratio
let withdrawn_from_vault = I80F48::from(cm!(before_vault - after_vault));
let position_native = account
.token_position_mut(payer_bank.token_index)?
.0
.native(&payer_bank);
if withdrawn_from_vault > position_native {
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
}
// Charge the difference in vault balance to the user's account
let vault_difference = {
let oracle_price =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;

View File

@ -181,20 +181,10 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
});
}
// Prevent borrowing away the full bank vault. Keep some in reserve to satisfy non-borrow withdraws
let bank_native_deposits = bank.native_deposits();
if bank_native_deposits != I80F48::ZERO && is_borrow {
// Enforce min vault to deposits ratio
if is_borrow {
ctx.accounts.vault.reload()?;
let bank_native_deposits: f64 = bank_native_deposits.checked_to_num().unwrap();
let vault_amount = ctx.accounts.vault.amount as f64;
if vault_amount < bank.min_vault_to_deposits_ratio * bank_native_deposits {
return err!(MangoError::BankBorrowLimitReached).with_context(|| {
format!(
"vault_amount ({:?}) below min_vault_to_deposits_ratio * bank_native_deposits ({:?})",
vault_amount, bank.min_vault_to_deposits_ratio * bank_native_deposits,
)
});
}
bank.enforce_min_vault_to_deposits_ratio(ctx.accounts.vault.as_ref())?;
}
Ok(())

View File

@ -214,8 +214,8 @@ pub mod mango_v4 {
instructions::account_edit(ctx, name_opt, delegate_opt)
}
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
instructions::account_close(ctx)
pub fn account_close(ctx: Context<AccountClose>, force_close: bool) -> Result<()> {
instructions::account_close(ctx, force_close)
}
// todo:

View File

@ -5,6 +5,7 @@ use crate::state::{oracle, StablePriceModel};
use crate::util;
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use derivative::Derivative;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
@ -234,6 +235,29 @@ impl Bank {
cm!(self.deposit_index * self.indexed_deposits)
}
/// Prevent borrowing away the full bank vault.
/// Keep some in reserve to satisfy non-borrow withdraws.
pub fn enforce_min_vault_to_deposits_ratio(&self, vault_ai: &AccountInfo) -> Result<()> {
require_keys_eq!(self.vault, vault_ai.key());
let vault = Account::<TokenAccount>::try_from(vault_ai)?;
let vault_amount = vault.amount as f64;
let bank_native_deposits = self.native_deposits();
if bank_native_deposits != I80F48::ZERO {
let bank_native_deposits: f64 = bank_native_deposits.checked_to_num().unwrap();
if vault_amount < self.min_vault_to_deposits_ratio * bank_native_deposits {
return err!(MangoError::BankBorrowLimitReached).with_context(|| {
format!(
"vault_amount ({:?}) below min_vault_to_deposits_ratio * bank_native_deposits ({:?})",
vault_amount, self.min_vault_to_deposits_ratio * bank_native_deposits,
)
});
}
}
Ok(())
}
/// Deposits `native_amount`.
///
/// If the token position ends up positive but below one native token and this token

View File

@ -1590,7 +1590,7 @@ impl ClientInstruction for AccountCloseInstruction {
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let instruction = Self::Instruction { force_close: false };
let accounts = Self::Accounts {
group: self.group,

View File

@ -2,7 +2,7 @@ import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai';
import _ from 'lodash';
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { BankForHealth, StablePriceModel, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { HealthType, PerpPosition } from './mangoAccount';
@ -412,7 +412,7 @@ describe('Health Cache', () => {
expect(
hc
.getMaxSourceForTokenSwap(
.getMaxSwapSourceForHealthRatio(
b0,
b1,
I80F48.fromNumber(2 / 3),
@ -425,9 +425,10 @@ describe('Health Cache', () => {
hc: HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: number,
minValue: number,
priceFactor: number,
): I80F48[] {
maxSwapFn: (HealthCache) => I80F48,
): number[] {
const clonedHc: HealthCache = _.cloneDeep(hc);
const sourcePrice = clonedHc.tokenInfos[source].prices;
@ -435,278 +436,428 @@ describe('Health Cache', () => {
const swapPrice = I80F48.fromNumber(priceFactor)
.mul(sourcePrice.oracle)
.div(targetPrice.oracle);
const sourceAmount = clonedHc.getMaxSourceForTokenSwap(
const sourceAmount = clonedHc.getMaxSwapSourceForHealthFn(
banks[source],
banks[target],
swapPrice,
I80F48.fromNumber(ratio),
I80F48.fromNumber(minValue),
maxSwapFn,
);
// adjust token balance
clonedHc.tokenInfos[source].balanceNative.isub(sourceAmount);
clonedHc.tokenInfos[target].balanceNative.iadd(
sourceAmount.mul(swapPrice),
);
function valueForAmount(amount: I80F48) {
// adjust token balance
const clonedHcClone: HealthCache = _.cloneDeep(clonedHc);
clonedHc.tokenInfos[source].balanceNative.isub(amount);
clonedHc.tokenInfos[target].balanceNative.iadd(amount.mul(swapPrice));
return maxSwapFn(clonedHcClone);
}
return [sourceAmount, clonedHc.healthRatio(HealthType.init)];
return [
sourceAmount.toNumber(),
valueForAmount(sourceAmount).toNumber(),
valueForAmount(sourceAmount.sub(ONE_I80F48())).toNumber(),
valueForAmount(sourceAmount.add(ONE_I80F48())).toNumber(),
];
}
function checkMaxSwapResult(
hc: HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: number,
minValue: number,
priceFactor: number,
maxSwapFn: (HealthCache) => I80F48,
): void {
const [sourceAmount, actualRatio] = findMaxSwapActual(
hc,
source,
target,
ratio,
priceFactor,
);
const [sourceAmount, actualValue, minusValue, plusValue] =
findMaxSwapActual(hc, source, target, minValue, priceFactor, maxSwapFn);
console.log(
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, amount: ${sourceAmount}`,
` -- checking ${source} to ${target} for priceFactor: ${priceFactor}, target: ${minValue} actual: ${minusValue}/${actualValue}/${plusValue}, amount: ${sourceAmount}`,
);
expect(Math.abs(actualRatio.toNumber() - ratio)).lessThan(1);
if (actualValue < minValue) {
// check that swapping more would decrease the ratio!
expect(plusValue < actualValue);
} else {
expect(actualValue >= minValue);
// either we're within tolerance of the target, or swapping 1 more would
// bring us below the target
expect(actualValue < minValue + 1 || plusValue < minValue);
}
}
{
console.log(' - test 0');
// adjust by usdc
const clonedHc = _.cloneDeep(hc);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
function maxSwapFnRatio(hc: HealthCache): I80F48 {
return hc.healthRatio(HealthType.init);
}
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
function maxSwapFn(hc: HealthCache): I80F48 {
return hc.health(HealthType.init);
}
for (const fn of [maxSwapFn, maxSwapFnRatio]) {
{
console.log(' - test 0');
// adjust by usdc
const clonedHc = _.cloneDeep(hc);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
// At this unlikely price it's healthy to swap infinitely
expect(function () {
findMaxSwapActual(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
50.0,
1.5,
fn,
);
}).to.throw('Number out of range');
}
{
console.log(' - test 1');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
// At this unlikely price it's healthy to swap infinitely
expect(function () {
findMaxSwapActual(
{
console.log(' - test 2');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
// possible even though the init ratio is <100
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
100,
1,
maxSwapFn,
);
}
{
console.log(' - test 3');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
);
// swapping with a high ratio advises paying back all liabs
// and then swapping even more because increasing assets in 0 has better asset weight
const initRatio = clonedHc.healthRatio(HealthType.init);
const [amount, actualRatio] = findMaxSwapActual(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
100,
1,
maxSwapFn,
);
expect(actualRatio / 2.0 > initRatio);
expect(amount - 100 / 3).lessThan(1);
}
{
console.log(' - test 4');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
);
const initRatio = clonedHc.healthRatio(HealthType.init);
expect(initRatio.toNumber()).greaterThan(3);
expect(initRatio.toNumber()).lessThan(4);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
50.0,
1.5,
);
}).to.throw('Number out of range');
}
{
console.log(' - test 1');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
}
}
}
{
console.log(' - test 2');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
// possible even though the init ratio is <100
checkMaxSwapResult(clonedHc, 1 as TokenIndex, 0 as TokenIndex, 100, 1);
}
{
console.log(' - test 3');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
);
// swapping with a high ratio advises paying back all liabs
// and then swapping even more because increasing assets in 0 has better asset weight
const initRatio = clonedHc.healthRatio(HealthType.init);
const [amount, actualRatio] = findMaxSwapActual(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
100,
1,
);
expect(actualRatio.div(I80F48.fromNumber(2)).toNumber()).greaterThan(
initRatio.toNumber(),
);
expect(amount.toNumber() - 100 / 3).lessThan(1);
}
{
console.log(' - test 4');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
);
const initRatio = clonedHc.healthRatio(HealthType.init);
expect(initRatio.toNumber()).greaterThan(3);
expect(initRatio.toNumber()).lessThan(4);
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 1, 1);
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 3, 1);
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 4, 1);
}
// TODO test 5
{
console.log(' - test 6');
const clonedHc = _.cloneDeep(hc);
clonedHc.serum3Infos = [
new Serum3Info(
I80F48.fromNumber(30 / 3),
I80F48.fromNumber(30 / 2),
1,
0,
0 as MarketIndex,
),
];
1,
maxSwapFn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
3,
1,
maxSwapFn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
4,
1,
maxSwapFn,
);
}
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
);
// TODO test 5
for (const priceFactor of [
// 0.9,
{
console.log(' - test 6');
const clonedHc = _.cloneDeep(hc);
clonedHc.serum3Infos = [
new Serum3Info(
I80F48.fromNumber(30 / 3),
I80F48.fromNumber(30 / 2),
1,
0,
0 as MarketIndex,
),
];
1.1,
]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
);
for (const priceFactor of [0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
// check starting with negative health but swapping can make it positive
console.log(' - test 7');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(20).div(clonedHc.tokenInfos[1].prices.oracle),
);
expect(clonedHc.health(HealthType.init) < 0);
for (const priceFactor of [0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
// check starting with negative health but swapping can't make it positive
console.log(' - test 8');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(10).div(clonedHc.tokenInfos[1].prices.oracle),
);
expect(clonedHc.health(HealthType.init) < 0);
for (const priceFactor of [0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
{
// swap some assets into a zero-asset-weight token
console.log(' - test 9');
const clonedHc = _.cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(10).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].initAssetWeight = ZERO_I80F48();
expect(
findMaxSwapActual(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
1,
1,
maxSwapFn,
)[0] > 0,
);
for (const priceFactor of [0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
fn,
);
}
}
}
}
done();
});

View File

@ -10,6 +10,7 @@ import {
ONE_I80F48,
ZERO_I80F48,
} from '../numbers/I80F48';
import { toNativeI80F48ForQuote } from '../utils';
import { Bank, BankForHealth, TokenIndex } from './bank';
import { Group } from './group';
@ -569,6 +570,72 @@ export class HealthCache {
throw new Error('Could not find amount that led to health ratio <=0');
}
/// This is not a generic function. It assumes there is a unique maximum between left and right.
private static findMaximum(
left: I80F48,
right: I80F48,
minStep: I80F48,
fun: (I80F48) => I80F48,
): I80F48[] {
const half = I80F48.fromNumber(0.5);
let mid = half.mul(left.add(right));
let leftValue = fun(left);
let rightValue = fun(right);
let midValue = fun(mid);
while (right.sub(left).gt(minStep)) {
if (leftValue.gte(midValue)) {
// max must be between left and mid
right = mid;
rightValue = midValue;
mid = half.mul(left.add(mid));
midValue = fun(mid);
} else if (midValue.lte(rightValue)) {
// max must be between mid and right
left = mid;
leftValue = midValue;
mid = half.mul(mid.add(right));
midValue = fun(mid);
} else {
// mid is larger than both left and right, max could be on either side
const leftmid = half.mul(left.add(mid));
const leftMidValue = fun(leftmid);
if (leftMidValue.gte(midValue)) {
// max between left and mid
right = mid;
rightValue = midValue;
mid = leftmid;
midValue = leftMidValue;
continue;
}
const rightmid = half.mul(mid.add(right));
const rightMidValue = fun(rightmid);
if (rightMidValue.gte(midValue)) {
// max between mid and right
left = mid;
leftValue = midValue;
mid = rightmid;
midValue = rightMidValue;
continue;
}
// max between leftmid and rightmid
left = leftmid;
leftValue = leftMidValue;
right = rightmid;
rightValue = rightMidValue;
}
}
if (leftValue.gte(midValue)) {
return [left, leftValue];
} else if (midValue.gte(rightValue)) {
return [mid, midValue];
} else {
return [right, rightValue];
}
}
private static binaryApproximationSearch(
left: I80F48,
leftValue: I80F48,
@ -577,10 +644,14 @@ export class HealthCache {
minStep: I80F48,
fun: (I80F48) => I80F48,
): I80F48 {
const maxIterations = 20;
const maxIterations = 40;
const targetError = I80F48.fromNumber(0.1);
const rightValue = fun(right);
// console.log(
// `binaryApproximationSearch left ${left}, leftValue ${leftValue}, right ${right}, rightValue ${rightValue}, targetValue ${targetValue}`,
// );
if (
(leftValue.sub(targetValue).isPos() &&
rightValue.sub(targetValue).isPos()) ||
@ -592,19 +663,22 @@ export class HealthCache {
);
}
let newAmount;
let newAmount, newAmountValue;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const key of Array(maxIterations).fill(0).keys()) {
if (right.sub(left).abs().lt(minStep)) {
return left;
}
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = fun(newAmount);
const error = newAmountRatio.sub(targetValue);
newAmountValue = fun(newAmount);
// console.log(
// ` - left ${left}, right ${right}, newAmount ${newAmount}, newAmountValue ${newAmountValue}, targetValue ${targetValue}`,
// );
const error = newAmountValue.sub(targetValue);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetValue) != rightValue.gt(targetValue)) {
if (newAmountValue.gt(targetValue) != rightValue.gt(targetValue)) {
left = newAmount;
} else {
right = newAmount;
@ -612,16 +686,74 @@ export class HealthCache {
}
console.error(
`Unable to get targetRatio within ${maxIterations} iterations`,
`Unable to get targetValue within ${maxIterations} iterations, newAmount ${newAmount}, newAmountValue ${newAmountValue}, target ${targetValue}`,
);
return newAmount;
}
getMaxSourceForTokenSwap(
getMaxSwapSource(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
): I80F48 {
const health = this.health(HealthType.init);
if (health.isNeg()) {
return this.getMaxSwapSourceForHealth(
sourceBank,
targetBank,
price,
toNativeI80F48ForQuote(1), // target 1 ui usd worth health
);
}
return this.getMaxSwapSourceForHealthRatio(
sourceBank,
targetBank,
price,
I80F48.fromNumber(2), // target 2% health
);
}
getMaxSwapSourceForHealthRatio(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
minRatio: I80F48,
): I80F48 {
return this.getMaxSwapSourceForHealthFn(
sourceBank,
targetBank,
price,
minRatio,
function (hc: HealthCache): I80F48 {
return hc.healthRatio(HealthType.init);
},
);
}
getMaxSwapSourceForHealth(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
minHealth: I80F48,
): I80F48 {
return this.getMaxSwapSourceForHealthFn(
sourceBank,
targetBank,
price,
minHealth,
function (hc: HealthCache): I80F48 {
return hc.health(HealthType.init);
},
);
}
getMaxSwapSourceForHealthFn(
sourceBank: BankForHealth,
targetBank: BankForHealth,
price: I80F48,
minFnValue: I80F48,
targetFn: (cache) => I80F48,
): I80F48 {
if (
sourceBank.initLiabWeight
@ -632,27 +764,29 @@ export class HealthCache {
return ZERO_I80F48();
}
// The health_ratio is a nonlinear based on swap amount.
// The health and health_ratio are nonlinear based on swap amount.
// For large swap amounts the slope is guaranteed to be negative, but small amounts
// can have positive slope (e.g. using source deposits to pay back target borrows).
//
// That means:
// - even if the initial ratio is < minRatio it can be useful to swap to *increase* health
// - be careful about finding the minRatio point: the function isn't convex
// - even if the initial value is < minRatio it can be useful to swap to *increase* health
// - even if initial value is < 0, swapping can increase health (maybe above 0)
// - be careful about finding the minFnValue: the function isn't convex
const initialRatio = this.healthRatio(HealthType.init);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const initialHealth = this.health(HealthType.init);
if (initialRatio.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const healthCacheClone: HealthCache = _.cloneDeep(this);
const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank);
const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank);
const source = healthCacheClone.tokenInfos[sourceIndex];
const target = healthCacheClone.tokenInfos[targetIndex];
const res = healthCacheClone.computeSerum3Reservations(HealthType.init);
const sourceReserved = res.tokenMaxReserved[sourceIndex];
const targetReserved = res.tokenMaxReserved[targetIndex];
// If the price is sufficiently good, then health will just increase from swapping:
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
// increases it by x * target_asset_weight * price_factor.
@ -688,126 +822,78 @@ export class HealthCache {
return adjustedCache;
}
function healthRatioAfterSwap(amount: I80F48): I80F48 {
return cacheAfterSwap(amount).healthRatio(HealthType.init);
function fnValueAfterSwap(amount: I80F48): I80F48 {
return targetFn(cacheAfterSwap(amount));
}
function healthAfterSwap(amount: I80F48): I80F48 {
return cacheAfterSwap(amount).health(HealthType.init);
}
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
// The function we're looking at has a unique maximum.
//
// If we discount serum3 reservations, there are two key slope changes:
// Assume source.balance > 0 and target.balance < 0.
// When these values flip sign, the health slope decreases, but could still be positive.
// After point1 it's definitely negative (due to finalHealthSlope check above).
// The maximum health ratio will be at 0 or at one of these points (ignoring serum3 effects).
const sourceForZeroTargetBalance = target.balanceNative.neg().div(price);
const point0Amount = source.balanceNative
.min(sourceForZeroTargetBalance)
.max(ZERO_I80F48());
const point1Amount = source.balanceNative
.max(sourceForZeroTargetBalance)
.max(ZERO_I80F48());
const cache0 = cacheAfterSwap(point0Amount);
const point0Ratio = cache0.healthRatio(HealthType.init);
const point0Health = cache0.health(HealthType.init);
const cache1 = cacheAfterSwap(point1Amount);
const point1Ratio = cache1.healthRatio(HealthType.init);
const point1Health = cache1.health(HealthType.init);
//
// The first thing we do is to find this maximum.
// The largest amount that the maximum could be at
const rightmost = source.balanceNative
.abs()
.add(sourceReserved)
.max(target.balanceNative.abs().add(targetReserved).div(price));
const [amountForMaxValue, maxValue] = HealthCache.findMaximum(
ZERO_I80F48(),
rightmost,
I80F48.fromNumber(0.1),
fnValueAfterSwap,
);
if (maxValue.lte(minFnValue)) {
// We cannot reach min_ratio, just return the max
return amountForMaxValue;
}
let amount: I80F48;
if (
initialRatio.lte(minRatio) &&
point0Ratio.lt(minRatio) &&
point1Ratio.lt(minRatio)
) {
// If we have to stay below the target ratio, pick the highest one
if (point0Ratio.gt(initialRatio)) {
if (point1Ratio.gt(point0Ratio)) {
amount = point1Amount;
} else {
amount = point0Amount;
}
} else if (point1Ratio.gt(initialRatio)) {
amount = point1Amount;
} else {
amount = ZERO_I80F48();
}
} else if (point1Ratio.gte(minRatio)) {
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
// search to the right of point1Amount: but how far?
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
// zero health: health - source_liab_weight * a + target_asset_weight * a * priceFactor = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
if (point1Health.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const zeroHealthEstimate = point1Amount.sub(
point1Health.div(finalHealthSlope),
);
const zeroHealthEstimateRatio = healthRatioAfterSwap(zeroHealthEstimate);
// console.log(`getMaxSourceForTokenSwap`);
// console.log(` - finalHealthSlope ${finalHealthSlope.toLocaleString()}`);
// console.log(` - minRatio ${minRatio.toLocaleString()}`);
// console.log(` - point0Amount ${point0Amount.toLocaleString()}`);
// console.log(` - point0Health ${point0Health.toLocaleString()}`);
// console.log(` - point0Ratio ${point0Ratio.toLocaleString()}`);
// console.log(` - point1Amount ${point1Amount.toLocaleString()}`);
// console.log(` - point1Health ${point1Health.toLocaleString()}`);
// console.log(` - point1Ratio ${point1Ratio.toLocaleString()}`);
// console.log(
// ` - zeroHealthEstimate ${zeroHealthEstimate.toLocaleString()}`,
// );
// console.log(
// ` - zeroHealthEstimateRatio ${zeroHealthEstimateRatio.toLocaleString()}`,
// );
const rightBound = HealthCache.scanRightUntilLessThan(
zeroHealthEstimate,
minRatio,
healthRatioAfterSwap,
);
if (rightBound.eq(zeroHealthEstimate)) {
amount = HealthCache.binaryApproximationSearch(
point1Amount,
point1Ratio,
rightBound,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
);
} else {
amount = HealthCache.binaryApproximationSearch(
zeroHealthEstimate,
healthRatioAfterSwap(zeroHealthEstimate),
rightBound,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
);
}
} else if (point0Ratio.gte(minRatio)) {
// Must be between point0Amount and point1Amount.
// Now max_value is bigger than minFnValue, the target amount must be >amountForMaxValue.
// Search to the right of amountForMaxValue: but how far?
// Use a simple estimation for the amount that would lead to zero health:
// health
// - source_liab_weight * source_liab_price * a
// + target_asset_weight * target_asset_price * price * a = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
const healthAtMaxValue = cacheAfterSwap(amountForMaxValue).health(
HealthType.init,
);
if (healthAtMaxValue.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const zeroHealthEstimate = amountForMaxValue.sub(
healthAtMaxValue.div(finalHealthSlope),
);
const rightBound = HealthCache.scanRightUntilLessThan(
zeroHealthEstimate,
minFnValue,
fnValueAfterSwap,
);
if (rightBound.eq(zeroHealthEstimate)) {
amount = HealthCache.binaryApproximationSearch(
point0Amount,
point0Ratio,
point1Amount,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
amountForMaxValue,
maxValue,
rightBound,
minFnValue,
I80F48.fromNumber(0.1),
fnValueAfterSwap,
);
} else {
// Must be between 0 and point0_amount
amount = HealthCache.binaryApproximationSearch(
ZERO_I80F48(),
initialRatio,
point0Amount,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
zeroHealthEstimate,
fnValueAfterSwap(zeroHealthEstimate),
rightBound,
minFnValue,
I80F48.fromNumber(0.1),
fnValueAfterSwap,
);
}
@ -1013,7 +1099,7 @@ export class HealthCache {
// Need to figure out how many lots to trade to reach zero health (zero_health_amount).
// We do this by looking at the starting health and the health slope per
// traded base lot (final_health_slope).
// traded base lot (finalHealthSlope).
const startCache = cacheAfterTrade(new BN(case1Start.toNumber()));
const startHealth = startCache.health(HealthType.init);
if (startHealth.lte(ZERO_I80F48())) {

View File

@ -505,16 +505,13 @@ export class MangoAccount {
/**
* The max amount of given source ui token you can swap to a target token.
* Price is simply the source tokens price divided by target tokens price,
* it is supposed to give an indication of how many source tokens can be traded for target tokens,
* it can optionally contain information on slippage and fees.
* @returns max amount of given source ui token you can swap to a target token, in ui token
*/
getMaxSourceUiForTokenSwap(
group: Group,
sourceMintPk: PublicKey,
targetMintPk: PublicKey,
price: number,
slippageAndFeesFactor = 1,
): number {
if (sourceMintPk.equals(targetMintPk)) {
return 0;
@ -522,11 +519,14 @@ export class MangoAccount {
const s = group.getFirstBankByMint(sourceMintPk);
const t = group.getFirstBankByMint(targetMintPk);
const hc = HealthCache.fromMangoAccount(group, this);
const maxSource = hc.getMaxSourceForTokenSwap(
const maxSource = hc.getMaxSwapSource(
s,
t,
I80F48.fromNumber(price * Math.pow(10, t.mintDecimals - s.mintDecimals)),
I80F48.fromNumber(2), // target 2% health
I80F48.fromNumber(
slippageAndFeesFactor *
((s.uiPrice / t.uiPrice) *
Math.pow(10, t.mintDecimals - s.mintDecimals)),
),
);
maxSource.idiv(
ONE_I80F48().add(
@ -831,13 +831,12 @@ export class MangoAccount {
public getMaxQuoteForPerpBidUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
price: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket,
I80F48.fromNumber(price),
I80F48.fromNumber(perpMarket.uiPrice),
PerpOrderSide.bid,
I80F48.fromNumber(2),
);
@ -859,13 +858,12 @@ export class MangoAccount {
public getMaxBaseForPerpAskUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
price: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const hc = HealthCache.fromMangoAccount(group, this);
const baseLots = hc.getMaxPerpForHealthRatio(
perpMarket,
I80F48.fromNumber(price),
I80F48.fromNumber(perpMarket.uiPrice),
PerpOrderSide.ask,
I80F48.fromNumber(2),
);
@ -876,7 +874,6 @@ export class MangoAccount {
group: Group,
perpMarketIndex: PerpMarketIndex,
size: number,
price: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
@ -889,7 +886,7 @@ export class MangoAccount {
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
PerpOrderSide.bid,
perpMarket.uiBaseToLots(size),
I80F48.fromNumber(price),
I80F48.fromNumber(perpMarket.uiPrice),
HealthType.init,
)
.toNumber();
@ -899,7 +896,6 @@ export class MangoAccount {
group: Group,
perpMarketIndex: PerpMarketIndex,
size: number,
price: number,
): number {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
@ -912,7 +908,7 @@ export class MangoAccount {
: PerpPosition.emptyFromPerpMarketIndex(perpMarket.perpMarketIndex),
PerpOrderSide.ask,
perpMarket.uiBaseToLots(size),
I80F48.fromNumber(price),
I80F48.fromNumber(perpMarket.uiPrice),
HealthType.init,
)
.toNumber();

View File

@ -764,12 +764,22 @@ export class MangoClient {
});
}
/**
* Note: this ix doesn't settle liabs, reduce open positions, or withdraw tokens to wallet,
* it simply closes the account. To close successfully ensure all positions are closed, or
* use forceClose flag
* @param group
* @param mangoAccount
* @param forceClose
* @returns
*/
public async closeMangoAccount(
group: Group,
mangoAccount: MangoAccount,
forceClose = false,
): Promise<TransactionSignature> {
const ix = await this.program.methods
.accountClose()
.accountClose(forceClose)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,

View File

@ -1,6 +1,5 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import { expect } from 'chai';
import fs from 'fs';
import { Group } from '../accounts/group';
import { HealthCache } from '../accounts/healthCache';
@ -26,7 +25,11 @@ async function debugUser(
group: Group,
mangoAccount: MangoAccount,
): Promise<void> {
console.log(mangoAccount.toString(group));
// Log only tokens
console.log(mangoAccount.toString(group, true));
// Turn on, to see serum and perp stuff
// console.log(mangoAccount.toString(group));
await mangoAccount.reload(client);
@ -98,15 +101,18 @@ async function debugUser(
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
// Turn on for debugging specific pairs
// if (src != 'DAI' || tgt != 'ETH') return;
// if (src != 'USDC' || tgt != 'MNGO') return;
const maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
group.banksMapByName.get(src)![0].uiPrice /
group.banksMapByName.get(tgt)![0].uiPrice,
);
let maxSourceUi;
try {
maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
);
} catch (error) {
console.log(`Error for ${src}->${tgt}, ` + error.toString());
}
const maxSourceWoFees =
-maxSourceUi *
@ -131,10 +137,6 @@ async function debugUser(
maxSourceUi.toFixed(3).padStart(10) +
`, health ratio after (${sim.toFixed(3).padStart(10)})`,
);
if (maxSourceUi > 0 && src !== tgt) {
expect(sim).gt(2);
expect(sim).lt(3);
}
}
for (const srcToken of Array.from(group.banksMapByName.keys()).sort()) {
for (const tgtToken of Array.from(group.banksMapByName.keys()).sort()) {
@ -146,24 +148,20 @@ async function debugUser(
const maxQuoteUi = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
const simMaxQuote = mangoAccount.simHealthRatioWithPerpBidUiChanges(
group,
perpMarket.perpMarketIndex,
maxQuoteUi / perpMarket.uiPrice,
perpMarket.uiPrice,
);
const maxBaseUi = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
const simMaxBase = mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
maxBaseUi,
perpMarket.uiPrice,
);
console.log(
`getMaxPerp ${perpMarket.name.padStart(
@ -178,10 +176,6 @@ async function debugUser(
.toFixed(3)
.padStart(10)})`,
);
if (maxQuoteUi > 0) {
expect(simMaxQuote).gt(2);
expect(simMaxQuote).lt(3);
}
}
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
@ -266,15 +260,11 @@ async function main(): Promise<void> {
true
// Enable below to debug specific mango accounts
// mangoAccount.publicKey.equals(
// new PublicKey('BXUPaeAWRCPvPdpndXJeykD8VYZJwrCBjZdWNZAu8Ca'),
// new PublicKey('GGfkfpT4dY8hmNK6SKKSBFdn7ucQXSc4WtDCQpnQt4p2'),
// )
) {
console.log();
console.log(`MangoAccount ${mangoAccount.publicKey}`);
// Log only tokens
// console.log(mangoAccount.toString(group, true));
// Long all debug info
await debugUser(client, group, mangoAccount);
}
}

View File

@ -1018,7 +1018,12 @@ export type MangoV4 = {
"isSigner": false
}
],
"args": []
"args": [
{
"name": "forceClose",
"type": "bool"
}
]
},
{
"name": "stubOracleCreate",
@ -4403,7 +4408,8 @@ export type MangoV4 = {
{
"name": "settlePnlLimitFactor",
"docs": [
"Fraction of perp base value that can be settled each window.",
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
"positive pnl that can be settled each window.",
"Set to a negative value to disable the limit."
],
"type": "f32"
@ -8421,7 +8427,12 @@ export const IDL: MangoV4 = {
"isSigner": false
}
],
"args": []
"args": [
{
"name": "forceClose",
"type": "bool"
}
]
},
{
"name": "stubOracleCreate",
@ -11806,7 +11817,8 @@ export const IDL: MangoV4 = {
{
"name": "settlePnlLimitFactor",
"docs": [
"Fraction of perp base value that can be settled each window.",
"Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized",
"positive pnl that can be settled each window.",
"Set to a negative value to disable the limit."
],
"type": "f32"

View File

@ -501,7 +501,6 @@ async function main() {
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
const baseQty = quoteQty / price;
console.log(
@ -509,7 +508,6 @@ async function main() {
group,
perpMarket.perpMarketIndex,
baseQty,
perpMarket.uiPrice,
)}`,
);
console.log(
@ -552,7 +550,6 @@ async function main() {
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
) * 1.02;
const baseQty = quoteQty / price;
@ -588,14 +585,12 @@ async function main() {
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
);
console.log(
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
baseQty,
perpMarket.uiPrice,
)}`,
);
const quoteQty = baseQty * price;
@ -628,11 +623,8 @@ async function main() {
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.uiPrice,
) * 1.02;
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) *
1.02;
const quoteQty = baseQty * price;
console.log(
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,

View File

@ -67,7 +67,6 @@ async function main() {
// close all banks
for (const banks of group.banksMapByMint.values()) {
console.log(banks[0].toString());
sig = await client.tokenDeregister(group, banks[0].mint);
console.log(
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`,

View File

@ -1,8 +1,14 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import {
AddressLookupTableProgram,
Connection,
Keypair,
PublicKey,
} from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { buildVersionedTx } from '../utils';
//
// Script which depoys a new mango group, and registers 3 tokens
@ -14,23 +20,23 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'],
['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
]);
const STUB_PRICES = new Map([
['USDC', 1.0],
['BTC', 20000.0], // btc and usdc both have 6 decimals
['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL
['MNGO', 0.04], // same price/decimals as SOL for convenience
['ETH', 1200.0], // eth and usdc both have 6 decimals
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
['MNGO', 0.02], // same price/decimals as SOL for convenience
]);
// 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([
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
['ETH/USDC', 'FZxi3yWkE5mMjyaZj6utmYL54QQYfMCKMcLaQZq4UwnA'],
['SOL/USDC', '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'],
]);
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
@ -130,17 +136,17 @@ async function main() {
}
// register token 1
console.log(`Registering BTC...`);
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
const btcMainnetOracle = oracles.get('BTC');
console.log(`Registering ETH...`);
const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
const ethMainnetOracle = oracles.get('ETH');
try {
await client.tokenRegister(
group,
btcMainnetMint,
btcMainnetOracle,
ethMainnetMint,
ethMainnetOracle,
defaultOracleConfig,
1,
'BTC',
'ETH',
defaultInterestRate,
0.0,
0.0001,
@ -215,7 +221,7 @@ async function main() {
0,
'MNGO-PERP',
defaultOracleConfig,
9,
6,
10,
100000, // base lots
0.9,
@ -235,14 +241,134 @@ async function main() {
0,
0,
0,
1.0,
-1.0,
2 * 60 * 60,
);
} catch (error) {
console.log(error);
}
await createAndPopulateAlt(client, admin);
process.exit();
}
main();
async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
let group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
const connection = client.program.provider.connection;
// Create ALT, and set to group at index 0
if (group.addressLookupTables[0].equals(PublicKey.default)) {
try {
console.log(`ALT: Creating`);
const createIx = AddressLookupTableProgram.createLookupTable({
authority: admin.publicKey,
payer: admin.publicKey,
recentSlot: await connection.getSlot('finalized'),
});
const createTx = await buildVersionedTx(
client.program.provider as AnchorProvider,
[createIx[0]],
);
let sig = await connection.sendTransaction(createTx);
console.log(
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`,
);
console.log(`ALT: set at index 0 for group...`);
sig = await client.altSet(group, createIx[1], 0);
console.log(`...https://explorer.solana.com/tx/${sig}`);
group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
} catch (error) {
console.log(error);
}
}
// Extend using mango v4 relevant pub keys
try {
let bankAddresses = Array.from(group.banksMapByMint.values())
.flat()
.map((bank) => [bank.publicKey, bank.oracle, bank.vault])
.flat()
.concat(
Array.from(group.banksMapByMint.values())
.flat()
.map((mintInfo) => mintInfo.publicKey),
);
let serum3MarketAddresses = Array.from(
group.serum3MarketsMapByExternal.values(),
)
.flat()
.map((serum3Market) => serum3Market.publicKey);
let serum3ExternalMarketAddresses = Array.from(
group.serum3ExternalMarketsMap.values(),
)
.flat()
.map((serum3ExternalMarket) => [
serum3ExternalMarket.publicKey,
serum3ExternalMarket.bidsAddress,
serum3ExternalMarket.asksAddress,
])
.flat();
let perpMarketAddresses = Array.from(
group.perpMarketsMapByMarketIndex.values(),
)
.flat()
.map((perpMarket) => [
perpMarket.publicKey,
perpMarket.oracle,
perpMarket.bids,
perpMarket.asks,
perpMarket.eventQueue,
])
.flat();
async function extendTable(addresses: PublicKey[]) {
await group.reloadAll(client);
const alt =
await client.program.provider.connection.getAddressLookupTable(
group.addressLookupTables[0],
);
addresses = addresses.filter(
(newAddress) =>
alt.value?.state.addresses &&
alt.value?.state.addresses.findIndex((addressInALt) =>
addressInALt.equals(newAddress),
) === -1,
);
if (addresses.length === 0) {
return;
}
const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: group.addressLookupTables[0],
payer: admin.publicKey,
authority: admin.publicKey,
addresses,
});
const extendTx = await buildVersionedTx(
client.program.provider as AnchorProvider,
[extendIx],
);
let sig = await client.program.provider.connection.sendTransaction(
extendTx,
);
console.log(`https://explorer.solana.com/tx/${sig}`);
}
console.log(`ALT: extending using mango v4 relevant public keys`);
await extendTable(bankAddresses);
await extendTable(serum3MarketAddresses);
await extendTable(serum3ExternalMarketAddresses);
await extendTable(perpMarketAddresses);
} catch (error) {
console.log(error);
}
}

View File

@ -1,79 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// This script deposits some tokens, so other liquidation scripts can borrow.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const ACCOUNT_NUM = Number(process.env.ACCOUNT_NUM || 0);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = (await client.createAndFetchMangoAccount(
group,
ACCOUNT_NUM,
'LIQTEST, FUNDING',
8,
4,
4,
4,
))!;
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
const usdcMint = group.banksMapByName.get('USDC')![0].mint;
const btcMint = group.banksMapByName.get('BTC')![0].mint;
const solMint = group.banksMapByName.get('SOL')![0].mint;
// deposit
try {
console.log(`...depositing 5 USDC`);
await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
await mangoAccount.reload(client);
console.log(`...depositing 0.0002 BTC`);
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
await mangoAccount.reload(client);
console.log(`...depositing 0.15 SOL`);
await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
await mangoAccount.reload(client);
} catch (error) {
console.log(error);
}
process.exit();
}
main();

View File

@ -1,8 +1,9 @@
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Bank } from '../accounts/bank';
import { MangoAccount } from '../accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
@ -19,24 +20,30 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
// native prices
const PRICES = {
BTC: 20000.0,
SOL: 0.04,
ETH: 1200.0,
SOL: 0.015,
USDC: 1,
MNGO: 0.04,
MNGO: 0.02,
};
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
]);
const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0],
['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920],
['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL],
['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', (18 * PRICES.BTC) / PRICES.SOL],
const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [
[
'LIQTEST, FUNDING',
[
['USDC', 5000000],
['ETH', 100000],
['SOL', 150000000],
],
[],
],
['LIQTEST, LIQOR', [['USDC', 1000000]], []],
['LIQTEST, A: USDC, L: SOL', [['USDC', 1000 * PRICES.SOL]], [['SOL', 920]]],
['LIQTEST, A: SOL, L: USDC', [['SOL', 1000]], [['USDC', 990 * PRICES.SOL]]],
[
'LIQTEST, A: ETH, L: SOL',
[['ETH', 20]],
[['SOL', (18 * PRICES.ETH) / PRICES.SOL]],
],
];
async function main() {
@ -66,17 +73,17 @@ async function main() {
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
const MAINNET_MINTS = new Map([
['USDC', group.banksMapByName.get('USDC')![0].mint],
['ETH', group.banksMapByName.get('ETH')![0].mint],
['SOL', group.banksMapByName.get('SOL')![0].mint],
]);
const accounts = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum));
const fundingAccount = accounts.find(
(account) => account.name == 'LIQTEST, FUNDING',
);
if (!fundingAccount) {
throw new Error('could not find funding account');
}
async function createMangoAccount(name: string): Promise<MangoAccount> {
const accountNum = maxAccountNum + 1;
@ -89,8 +96,73 @@ async function main() {
))!;
}
async function setBankPrice(bank: Bank, price: number): Promise<void> {
await client.stubOracleSet(group, bank.oracle, price);
// reset stable price
await client.tokenEdit(
group,
bank.mint,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
true,
null,
);
}
async function setPerpPrice(
perpMarket: PerpMarket,
price: number,
): Promise<void> {
await client.stubOracleSet(group, perpMarket.oracle, price);
// reset stable price
await client.perpEditMarket(
group,
perpMarket.perpMarketIndex,
perpMarket.oracle,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
);
}
for (const scenario of TOKEN_SCENARIOS) {
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
const [name, assets, liabs] = scenario;
// create account
console.log(`Creating mangoaccount...`);
@ -99,22 +171,24 @@ async function main() {
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
for (let [assetName, assetAmount] of assets) {
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
await client.tokenDepositNative(
group,
mangoAccount,
assetMint,
new BN(assetAmount),
);
await mangoAccount.reload(client);
}
await client.tokenDepositNative(
group,
mangoAccount,
assetMint,
new BN(assetAmount),
);
await mangoAccount.reload(client);
for (let [liabName, liabAmount] of liabs) {
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
if (liabAmount > 0) {
// temporarily drop the borrowed token value, so the borrow goes through
const oracle = group.banksMapByName.get(liabName)![0].oracle;
const bank = group.banksMapByName.get(liabName)![0];
try {
await client.stubOracleSet(group, oracle, PRICES[liabName] / 2);
await setBankPrice(bank, PRICES[liabName] / 2);
await client.tokenWithdrawNative(
group,
@ -125,11 +199,22 @@ async function main() {
);
} finally {
// restore the oracle
await client.stubOracleSet(group, oracle, PRICES[liabName]);
await setBankPrice(bank, PRICES[liabName]);
}
}
}
const accounts2 = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
const fundingAccount = accounts2.find(
(account) => account.name == 'LIQTEST, FUNDING',
);
if (!fundingAccount) {
throw new Error('could not find funding account');
}
// Serum order scenario
{
const name = 'LIQTEST, serum orders';
@ -233,19 +318,18 @@ async function main() {
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
const collateralBank = group.banksMapByName.get('SOL')![0];
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
new BN(100000),
); // valued as $0.004 maint collateral
new BN(300000),
); // valued as 0.0003 SOL, $0.0045 maint collateral
await mangoAccount.reload(client);
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4);
await setBankPrice(collateralBank, PRICES['SOL'] * 4);
try {
await client.perpPlaceOrder(
@ -253,9 +337,9 @@ async function main() {
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
1, // ui price that won't get hit
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
0.001, // ui price that won't get hit
1.1, // ui base quantity, 11 base lots, 1.1 MNGO, $0.022
0.022, // ui quote quantity
4200,
PerpOrderType.limit,
false,
@ -263,7 +347,7 @@ async function main() {
5,
);
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
await setBankPrice(collateralBank, PRICES['SOL']);
}
}
@ -277,19 +361,18 @@ async function main() {
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
const collateralBank = group.banksMapByName.get('SOL')![0];
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
new BN(100000),
); // valued as $0.004 maint collateral
new BN(300000),
); // valued as 0.0003 SOL, $0.0045 maint collateral
await mangoAccount.reload(client);
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5);
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
try {
await client.perpPlaceOrder(
@ -297,9 +380,9 @@ async function main() {
fundingAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.ask,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
0.03,
1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033
0.033, // ui quote quantity
4200,
PerpOrderType.limit,
false,
@ -312,9 +395,9 @@ async function main() {
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
0.03,
1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033
0.033, // ui quote quantity
4200,
PerpOrderType.market,
false,
@ -327,7 +410,7 @@ async function main() {
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
await setBankPrice(collateralBank, PRICES['SOL']);
}
}
@ -341,23 +424,22 @@ async function main() {
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
const baseOracle = (await client.getStubOracle(group, baseMint))[0]
.publicKey;
const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!;
const perpIndex = perpMarket.perpMarketIndex;
const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
const collateralBank = group.banksMapByName.get('SOL')![0];
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
new BN(100000),
); // valued as $0.004 maint collateral
new BN(300000),
); // valued as $0.0045 maint collateral
await mangoAccount.reload(client);
try {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10);
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
// Spot-borrow more than the collateral is worth
await client.tokenWithdrawNative(
@ -369,16 +451,16 @@ async function main() {
);
await mangoAccount.reload(client);
// Execute two trades that leave the account with +$0.022 positive pnl
await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2);
// Execute two trades that leave the account with +$0.011 positive pnl
await setPerpPrice(perpMarket, PRICES['MNGO'] / 2);
await client.perpPlaceOrder(
group,
fundingAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
perpIndex,
PerpOrderSide.ask,
20,
0.0011, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
0.01,
1.1, // ui base quantity, 11 base lots, $0.011
0.011, // ui quote quantity
4200,
PerpOrderType.limit,
false,
@ -388,32 +470,29 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
perpIndex,
PerpOrderSide.bid,
20,
0.0011, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
0.01,
1.1, // ui base quantity, 11 base lots, $0.011
0.011, // ui quote quantity
4200,
PerpOrderType.market,
false,
0,
5,
);
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
await client.perpConsumeAllEvents(group, perpIndex);
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
await setPerpPrice(perpMarket, PRICES['MNGO']);
await client.perpPlaceOrder(
group,
fundingAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
perpIndex,
PerpOrderSide.bid,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
0.02,
1.1, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
4201,
PerpOrderType.limit,
false,
@ -423,24 +502,21 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
perpIndex,
PerpOrderSide.ask,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
0.02,
1.1, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
4201,
PerpOrderType.market,
false,
0,
5,
);
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
await client.perpConsumeAllEvents(group, perpIndex);
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
await setPerpPrice(perpMarket, PRICES['MNGO']);
await setBankPrice(collateralBank, PRICES['SOL']);
}
}

View File

@ -80,7 +80,7 @@ async function main() {
// close account
try {
console.log(`closing account: ${account}`);
await client.closeMangoAccount(group, account);
await client.closeMangoAccount(group, account, true);
} catch (error) {
console.log(`failed to close ${account.publicKey}: ${error}`);
}

View File

@ -22,6 +22,10 @@ import { I80F48 } from './numbers/I80F48';
export const U64_MAX_BN = new BN('18446744073709551615');
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
export function toNativeI80F48ForQuote(uiAmount: number): I80F48 {
return I80F48.fromNumber(uiAmount * Math.pow(10, 6));
}
export function toNativeI80F48(uiAmount: number, decimals: number): I80F48 {
return I80F48.fromNumber(uiAmount * Math.pow(10, decimals));
}