Merge branch 'main' into deploy
This commit is contained in:
commit
fb6311e842
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -4,7 +4,33 @@ Update this for each program release and mainnet deployment.
|
|||
|
||||
## not on mainnet
|
||||
|
||||
### v0.23.0, 2024-3-
|
||||
### v0.24.0, 2024-4-
|
||||
|
||||
- Allow skipping banks and invalid oracles when computing health (#891)
|
||||
|
||||
This is only possible when we know for sure that the operation would not put the account into negative health zone.
|
||||
|
||||
- Add support for Raydium CLMM as oracle fallback (#856)
|
||||
|
||||
- Add a `TokenBalanceLog` when charging collateral fees (#894)
|
||||
|
||||
- Withdraw instruction: remove overflow error and return appropriate error message instead (#910)
|
||||
|
||||
- Banks: add more safety checks (#895)
|
||||
|
||||
- Add a health check instruction (#913)
|
||||
|
||||
Assert in a transaction that operation run on a mango account does not reduce it's health below a specified amount.
|
||||
|
||||
- Add a sequence check instruction (#909)
|
||||
|
||||
Assert that a transaction was emitted and run with a correct view of the current mango state.
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.23.0, 2024-3-8
|
||||
|
||||
Deployment: Mar 8, 2024 at 12:10:52 Central European Standard Time, https://explorer.solana.com/tx/6MXGookZoYGMYb7tWrrmgZzVA13HJimHNqwHRVFeqL9YpQD7YasH1pQn4MSQTK1o13ixKTGFxwZsviUzmHzzP9m
|
||||
|
||||
- Allow disabling asset liquidations for tokens (#867)
|
||||
|
||||
|
@ -26,8 +52,6 @@ Update this for each program release and mainnet deployment.
|
|||
- Flash loan: Add a "swap without flash loan fees" option (#882)
|
||||
- Cleanup, tests and minor (#878, #875, #854, #838, #895)
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.22.0, 2024-3-3
|
||||
|
||||
Deployment: Mar 3, 2024 at 23:52:08 Central European Standard Time, https://explorer.solana.com/tx/3MpEMU12Pv7RpSnwfShoM9sbyr41KAEeJFCVx9ypkq8nuK8Q5vm7CRLkdhH3u91yQ4k44a32armZHaoYguX6NqsY
|
||||
|
|
|
@ -2455,6 +2455,20 @@ version = "0.14.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
|
||||
[[package]]
|
||||
name = "hdrhistogram"
|
||||
version = "7.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"byteorder",
|
||||
"crossbeam-channel",
|
||||
"flate2 1.0.27",
|
||||
"nom 7.1.3",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
version = "0.3.9"
|
||||
|
@ -3367,15 +3381,17 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
"anyhow",
|
||||
"arrayref",
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"bincode",
|
||||
"borsh 0.10.3",
|
||||
"bs58 0.5.0",
|
||||
"bytemuck",
|
||||
"default-env",
|
||||
"derivative",
|
||||
|
@ -3391,6 +3407,7 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serum_dex 0.5.10 (git+https://github.com/openbook-dex/program.git)",
|
||||
"solana-address-lookup-table-program",
|
||||
"solana-logger",
|
||||
|
@ -3522,6 +3539,7 @@ dependencies = [
|
|||
"futures 0.3.28",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hdrhistogram",
|
||||
"itertools",
|
||||
"jemallocator",
|
||||
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
13
RELEASING.md
13
RELEASING.md
|
@ -7,19 +7,18 @@
|
|||
- 4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg is the address of the Mango v4 Program
|
||||
- FP4PxqHTVzeG2c6eZd7974F9WvKUSdBeduUK3rjYyvBw is the address of the Mango v4 Program Governance
|
||||
|
||||
- Check out the latest version of the `dev` branch
|
||||
- Assuming there's a release branch (like release/program-v0.22.0)
|
||||
with a completed audit and an updated changelog.
|
||||
|
||||
- Update the changelog
|
||||
|
||||
git log program-v0.11.0..HEAD -- programs/mango-v4/
|
||||
- Check out the release branch
|
||||
|
||||
- Make sure the version is bumped in programs/mango-v4/Cargo.toml
|
||||
|
||||
- Update the idl ./update-local-idl.sh
|
||||
- Update the idl ./update-local-idl.sh and verify that there's no difference
|
||||
|
||||
- Run the tests to double check
|
||||
- Run the tests to double check there are no failures
|
||||
|
||||
- Tag and push
|
||||
- Tag (`git tag program-v0.xy.z HEAD`) and push it (`git push <tag>`)
|
||||
|
||||
- Do a verifiable build
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -28,12 +28,13 @@ pub async fn save_snapshot(
|
|||
|
||||
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
|
||||
|
||||
let oracles_and_vaults = group_context
|
||||
let extra_accounts = group_context
|
||||
.tokens
|
||||
.values()
|
||||
.map(|value| value.oracle)
|
||||
.chain(group_context.perp_markets.values().map(|p| p.oracle))
|
||||
.chain(group_context.tokens.values().flat_map(|value| value.vaults))
|
||||
.chain(group_context.address_lookup_tables.iter().copied())
|
||||
.unique()
|
||||
.filter(|pk| *pk != Pubkey::default())
|
||||
.collect::<Vec<Pubkey>>();
|
||||
|
@ -55,7 +56,7 @@ pub async fn save_snapshot(
|
|||
serum_programs,
|
||||
open_orders_authority: mango_group,
|
||||
},
|
||||
oracles_and_vaults.clone(),
|
||||
extra_accounts.clone(),
|
||||
account_update_sender.clone(),
|
||||
);
|
||||
|
||||
|
@ -75,7 +76,7 @@ pub async fn save_snapshot(
|
|||
snapshot_interval: Duration::from_secs(6000),
|
||||
min_slot: first_websocket_slot + 10,
|
||||
},
|
||||
oracles_and_vaults,
|
||||
extra_accounts,
|
||||
account_update_sender,
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
|
|
|
@ -49,3 +49,4 @@ tokio-stream = { version = "0.1.9"}
|
|||
tokio-tungstenite = "0.16.1"
|
||||
tracing = "0.1"
|
||||
regex = "1.9.5"
|
||||
hdrhistogram = "7.5.4"
|
|
@ -287,6 +287,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let mut metric_account_update_queue_len =
|
||||
metrics.register_u64("account_update_queue_length".into());
|
||||
let mut metric_chain_update_latency =
|
||||
metrics.register_latency("in-memory chain update".into());
|
||||
let mut metric_mango_accounts = metrics.register_u64("mango_accounts".into());
|
||||
|
||||
let mut mint_infos = HashMap::<TokenIndex, Pubkey>::new();
|
||||
|
@ -299,6 +301,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
.recv()
|
||||
.await
|
||||
.expect("channel not closed");
|
||||
let current_time = Instant::now();
|
||||
metric_account_update_queue_len.set(account_update_receiver.len() as u64);
|
||||
|
||||
message.update_chain_data(&mut chain_data.write().unwrap());
|
||||
|
@ -306,6 +309,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
match message {
|
||||
Message::Account(account_write) => {
|
||||
let mut state = shared_state.write().unwrap();
|
||||
let reception_time = account_write.reception_time;
|
||||
state.oldest_chain_event_reception_time = Some(
|
||||
state
|
||||
.oldest_chain_event_reception_time
|
||||
.unwrap_or(reception_time),
|
||||
);
|
||||
|
||||
metric_chain_update_latency.push(current_time - reception_time);
|
||||
|
||||
if is_mango_account(&account_write.account, &mango_group).is_some() {
|
||||
// e.g. to render debug logs RUST_LOG="liquidator=debug"
|
||||
debug!(
|
||||
|
@ -320,8 +332,21 @@ async fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
Message::Snapshot(snapshot) => {
|
||||
let mut state = shared_state.write().unwrap();
|
||||
let mut reception_time = None;
|
||||
|
||||
// Track all mango account pubkeys
|
||||
for update in snapshot.iter() {
|
||||
reception_time = Some(
|
||||
update
|
||||
.reception_time
|
||||
.min(reception_time.unwrap_or(update.reception_time)),
|
||||
);
|
||||
state.oldest_chain_event_reception_time = Some(
|
||||
state
|
||||
.oldest_chain_event_reception_time
|
||||
.unwrap_or(update.reception_time),
|
||||
);
|
||||
|
||||
if is_mango_account(&update.account, &mango_group).is_some() {
|
||||
state.mango_accounts.insert(update.pubkey);
|
||||
}
|
||||
|
@ -335,6 +360,11 @@ async fn main() -> anyhow::Result<()> {
|
|||
oracles.insert(perp_market.oracle);
|
||||
}
|
||||
}
|
||||
|
||||
if reception_time.is_some() {
|
||||
metric_chain_update_latency
|
||||
.push(current_time - reception_time.unwrap());
|
||||
}
|
||||
metric_mango_accounts.set(state.mango_accounts.len() as u64);
|
||||
|
||||
state.one_snapshot_done = true;
|
||||
|
@ -374,35 +404,82 @@ async fn main() -> anyhow::Result<()> {
|
|||
let liquidation_job = tokio::spawn({
|
||||
let mut interval =
|
||||
mango_v4_client::delay_interval(Duration::from_millis(cli.check_interval_ms));
|
||||
let mut metric_liquidation_check = metrics.register_latency("liquidation_check".into());
|
||||
let mut metric_liquidation_start_end =
|
||||
metrics.register_latency("liquidation_start_end".into());
|
||||
|
||||
let mut liquidation_start_time = None;
|
||||
let mut tcs_start_time = None;
|
||||
|
||||
let shared_state = shared_state.clone();
|
||||
async move {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let account_addresses = {
|
||||
let state = shared_state.write().unwrap();
|
||||
let mut state = shared_state.write().unwrap();
|
||||
if !state.one_snapshot_done {
|
||||
// discard first latency info as it will skew data too much
|
||||
state.oldest_chain_event_reception_time = None;
|
||||
continue;
|
||||
}
|
||||
if state.oldest_chain_event_reception_time.is_none()
|
||||
&& liquidation_start_time.is_none()
|
||||
{
|
||||
// no new update, skip computing
|
||||
continue;
|
||||
}
|
||||
|
||||
state.mango_accounts.iter().cloned().collect_vec()
|
||||
};
|
||||
|
||||
liquidation.errors.update();
|
||||
liquidation.oracle_errors.update();
|
||||
|
||||
if liquidation_start_time.is_none() {
|
||||
liquidation_start_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
let liquidated = liquidation
|
||||
.maybe_liquidate_one(account_addresses.iter())
|
||||
.await;
|
||||
|
||||
if !liquidated {
|
||||
// This will be incorrect if we liquidate the last checked account
|
||||
// (We will wait for next full run, skewing latency metrics)
|
||||
// Probability is very low, might not need to be fixed
|
||||
|
||||
let mut state = shared_state.write().unwrap();
|
||||
let reception_time = state.oldest_chain_event_reception_time.unwrap();
|
||||
let current_time = Instant::now();
|
||||
|
||||
state.oldest_chain_event_reception_time = None;
|
||||
|
||||
metric_liquidation_check.push(current_time - reception_time);
|
||||
metric_liquidation_start_end
|
||||
.push(current_time - liquidation_start_time.unwrap());
|
||||
liquidation_start_time = None;
|
||||
}
|
||||
|
||||
let mut took_tcs = false;
|
||||
if !liquidated && cli.take_tcs == BoolArg::True {
|
||||
tcs_start_time = Some(tcs_start_time.unwrap_or(Instant::now()));
|
||||
|
||||
took_tcs = liquidation
|
||||
.maybe_take_token_conditional_swap(account_addresses.iter())
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
error!("error during maybe_take_token_conditional_swap: {err}");
|
||||
false
|
||||
})
|
||||
});
|
||||
|
||||
if !took_tcs {
|
||||
let current_time = Instant::now();
|
||||
let mut metric_tcs_start_end =
|
||||
metrics.register_latency("tcs_start_end".into());
|
||||
metric_tcs_start_end.push(current_time - tcs_start_time.unwrap());
|
||||
tcs_start_time = None;
|
||||
}
|
||||
}
|
||||
|
||||
if liquidated || took_tcs {
|
||||
|
@ -483,6 +560,9 @@ struct SharedState {
|
|||
|
||||
/// Is the first snapshot done? Only start checking account health when it is.
|
||||
one_snapshot_done: bool,
|
||||
|
||||
/// Oldest chain event not processed yet
|
||||
oldest_chain_event_reception_time: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use hdrhistogram::Histogram;
|
||||
use std::time::Duration;
|
||||
use {
|
||||
std::collections::HashMap,
|
||||
std::sync::{atomic, Arc, Mutex, RwLock},
|
||||
|
@ -10,6 +12,7 @@ enum Value {
|
|||
U64(Arc<atomic::AtomicU64>),
|
||||
I64(Arc<atomic::AtomicI64>),
|
||||
String(Arc<Mutex<String>>),
|
||||
Latency(Arc<Mutex<Histogram<u64>>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -49,6 +52,18 @@ impl MetricU64 {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MetricLatency {
|
||||
value: Arc<Mutex<Histogram<u64>>>,
|
||||
}
|
||||
impl MetricLatency {
|
||||
pub fn push(&mut self, duration: std::time::Duration) {
|
||||
let mut guard = self.value.lock().unwrap();
|
||||
let ns: u64 = duration.as_nanos().try_into().unwrap();
|
||||
guard.record(ns).expect("latency error");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MetricI64 {
|
||||
value: Arc<atomic::AtomicI64>,
|
||||
|
@ -110,6 +125,19 @@ impl Metrics {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn register_latency(&self, name: String) -> MetricLatency {
|
||||
let mut registry = self.registry.write().unwrap();
|
||||
let value = registry.entry(name).or_insert_with(|| {
|
||||
Value::Latency(Arc::new(Mutex::new(Histogram::<u64>::new(3).unwrap())))
|
||||
});
|
||||
MetricLatency {
|
||||
value: match value {
|
||||
Value::Latency(v) => v.clone(),
|
||||
_ => panic!("bad metric type"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_string(&self, name: String) -> MetricString {
|
||||
let mut registry = self.registry.write().unwrap();
|
||||
let value = registry
|
||||
|
@ -187,6 +215,16 @@ pub fn start() -> Metrics {
|
|||
);
|
||||
}
|
||||
}
|
||||
Value::Latency(v) => {
|
||||
let hist = v.lock().unwrap();
|
||||
|
||||
info!(
|
||||
"metric: {}: 99'th percentile: {:?}, 99,9'th percentile: {:?}",
|
||||
name,
|
||||
Duration::from_nanos(hist.value_at_quantile(0.99)),
|
||||
Duration::from_nanos(hist.value_at_quantile(0.999))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use solana_client::rpc_response::{Response, RpcKeyedAccount};
|
||||
use solana_sdk::{account::AccountSharedData, pubkey::Pubkey};
|
||||
|
||||
use std::time::Instant;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use tracing::*;
|
||||
|
||||
|
@ -11,6 +12,7 @@ pub struct AccountUpdate {
|
|||
pub pubkey: Pubkey,
|
||||
pub slot: u64,
|
||||
pub account: AccountSharedData,
|
||||
pub reception_time: Instant,
|
||||
}
|
||||
|
||||
impl AccountUpdate {
|
||||
|
@ -25,15 +27,22 @@ impl AccountUpdate {
|
|||
pubkey,
|
||||
slot: rpc.context.slot,
|
||||
account,
|
||||
reception_time: Instant::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChainSlotUpdate {
|
||||
pub slot_update: Arc<solana_client::rpc_response::SlotUpdate>,
|
||||
pub reception_time: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Message {
|
||||
Account(AccountUpdate),
|
||||
Snapshot(Vec<AccountUpdate>),
|
||||
Slot(Arc<solana_client::rpc_response::SlotUpdate>),
|
||||
Slot(ChainSlotUpdate),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
|
@ -65,7 +74,7 @@ impl Message {
|
|||
}
|
||||
Message::Slot(slot_update) => {
|
||||
trace!("websocket slot message");
|
||||
let slot_update = match **slot_update {
|
||||
let slot_update = match *(slot_update.slot_update) {
|
||||
solana_client::rpc_response::SlotUpdate::CreatedBank {
|
||||
slot, parent, ..
|
||||
} => Some(SlotData {
|
||||
|
|
|
@ -7,9 +7,9 @@ use anchor_lang::__private::bytemuck;
|
|||
use mango_v4::{
|
||||
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
|
||||
state::{
|
||||
determine_oracle_type, load_whirlpool_state, oracle_state_unchecked, Group,
|
||||
MangoAccountValue, OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType,
|
||||
PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
|
||||
determine_oracle_type, load_orca_pool_state, load_raydium_pool_state,
|
||||
oracle_state_unchecked, Group, MangoAccountValue, OracleAccountInfos, OracleConfig,
|
||||
OracleConfigParams, OracleType, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -721,10 +721,14 @@ async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Acco
|
|||
fn get_fallback_quote_key(acc_info: &impl KeyedAccountReader) -> Pubkey {
|
||||
let maybe_key = match determine_oracle_type(acc_info).ok() {
|
||||
Some(oracle_type) => match oracle_type {
|
||||
OracleType::OrcaCLMM => match load_whirlpool_state(acc_info).ok() {
|
||||
OracleType::OrcaCLMM => match load_orca_pool_state(acc_info).ok() {
|
||||
Some(whirlpool) => whirlpool.get_quote_oracle().ok(),
|
||||
None => None,
|
||||
},
|
||||
OracleType::RaydiumCLMM => match load_raydium_pool_state(acc_info).ok() {
|
||||
Some(pool) => pool.get_quote_oracle().ok(),
|
||||
None => None,
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
|
|
|
@ -15,8 +15,7 @@ use futures::{stream, StreamExt};
|
|||
use solana_rpc::rpc::rpc_accounts::AccountsDataClient;
|
||||
use solana_rpc::rpc::rpc_accounts_scan::AccountsScanClient;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::task::JoinHandle;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time;
|
||||
use tracing::*;
|
||||
|
||||
|
@ -56,6 +55,7 @@ impl AccountSnapshot {
|
|||
.account
|
||||
.decode()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
|
||||
reception_time: Instant::now(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
|
@ -75,6 +75,7 @@ impl AccountSnapshot {
|
|||
account: ui_account
|
||||
.decode()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
|
||||
reception_time: Instant::now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ use solana_rpc::rpc_pubsub::RpcSolPubSubClient;
|
|||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
|
||||
|
||||
use anyhow::Context;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio_stream::StreamMap;
|
||||
use tracing::*;
|
||||
|
||||
use crate::account_update_stream::{AccountUpdate, Message};
|
||||
use crate::account_update_stream::{AccountUpdate, ChainSlotUpdate, Message};
|
||||
use crate::AnyhowWrap;
|
||||
|
||||
pub struct Config {
|
||||
|
@ -143,7 +143,10 @@ async fn feed_data(
|
|||
},
|
||||
message = slot_sub.next() => {
|
||||
if let Some(data) = message {
|
||||
sender.send(Message::Slot(data.map_err_anyhow()?)).await.expect("sending must succeed");
|
||||
sender.send(Message::Slot(ChainSlotUpdate{
|
||||
slot_update: data.map_err_anyhow()?,
|
||||
reception_time: Instant::now()
|
||||
})).await.expect("sending must succeed");
|
||||
} else {
|
||||
warn!("slot update stream closed");
|
||||
return Ok(());
|
||||
|
@ -200,7 +203,7 @@ pub async fn get_next_create_bank_slot(
|
|||
match msg {
|
||||
Message::Slot(slot_update) => {
|
||||
if let solana_client::rpc_response::SlotUpdate::CreatedBank { slot, .. } =
|
||||
*slot_update
|
||||
*slot_update.slot_update
|
||||
{
|
||||
return Ok(slot);
|
||||
}
|
||||
|
|
130
mango_v4.json
130
mango_v4.json
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.23.0",
|
||||
"version": "0.24.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -1760,6 +1760,66 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sequenceCheck",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "expectedSequenceNumber",
|
||||
"type": "u8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "healthCheck",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "minHealthValue",
|
||||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "checkKind",
|
||||
"type": {
|
||||
"defined": "HealthCheckKind"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stubOracleCreate",
|
||||
"accounts": [
|
||||
|
@ -7871,13 +7931,8 @@
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1
|
||||
]
|
||||
}
|
||||
"name": "sequenceNumber",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "netDeposits",
|
||||
|
@ -9669,13 +9724,8 @@
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1
|
||||
]
|
||||
}
|
||||
"name": "sequenceNumber",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "netDeposits",
|
||||
|
@ -10654,6 +10704,32 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "HealthCheckKind",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Maint"
|
||||
},
|
||||
{
|
||||
"name": "Init"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEnd"
|
||||
},
|
||||
{
|
||||
"name": "MaintRatio"
|
||||
},
|
||||
{
|
||||
"name": "InitRatio"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEndRatio"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Serum3SelfTradeBehavior",
|
||||
"docs": [
|
||||
|
@ -11008,6 +11084,12 @@
|
|||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
},
|
||||
{
|
||||
"name": "SequenceCheck"
|
||||
},
|
||||
{
|
||||
"name": "HealthCheck"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -11048,6 +11130,9 @@
|
|||
},
|
||||
{
|
||||
"name": "OrcaCLMM"
|
||||
},
|
||||
{
|
||||
"name": "RaydiumCLMM"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -14347,6 +14432,21 @@
|
|||
"code": 6069,
|
||||
"name": "TokenAssetLiquidationDisabled",
|
||||
"msg": "the asset does not allow liquidation"
|
||||
},
|
||||
{
|
||||
"code": 6070,
|
||||
"name": "BorrowsRequireHealthAccountBank",
|
||||
"msg": "for borrows the bank must be in the health account list"
|
||||
},
|
||||
{
|
||||
"code": 6071,
|
||||
"name": "InvalidSequenceNumber",
|
||||
"msg": "invalid sequence number"
|
||||
},
|
||||
{
|
||||
"code": 6072,
|
||||
"name": "InvalidHealth",
|
||||
"msg": "invalid health"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mango-v4"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -75,3 +75,6 @@ rand = "0.8.4"
|
|||
lazy_static = "1.4.0"
|
||||
num = "0.4.0"
|
||||
regex = "1"
|
||||
serde_json = "1"
|
||||
bs58 = "0.5"
|
||||
anyhow = "1"
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,30 @@
|
|||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
#[derive(Clone, Copy, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum HealthCheckKind {
|
||||
Maint = 0b0000,
|
||||
Init = 0b0010,
|
||||
LiquidationEnd = 0b0100,
|
||||
MaintRatio = 0b0001,
|
||||
InitRatio = 0b0011,
|
||||
LiquidationEndRatio = 0b0101,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct HealthCheck<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.is_ix_enabled(IxGate::HealthCheck) @ MangoError::IxIsDisabled,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccountFixed>,
|
||||
}
|
|
@ -16,6 +16,7 @@ pub use group_close::*;
|
|||
pub use group_create::*;
|
||||
pub use group_edit::*;
|
||||
pub use group_withdraw_insurance_fund::*;
|
||||
pub use health_check::*;
|
||||
pub use health_region::*;
|
||||
pub use ix_gate_set::*;
|
||||
pub use openbook_v2_cancel_order::*;
|
||||
|
@ -45,6 +46,7 @@ pub use perp_place_order::*;
|
|||
pub use perp_settle_fees::*;
|
||||
pub use perp_settle_pnl::*;
|
||||
pub use perp_update_funding::*;
|
||||
pub use sequence_check::*;
|
||||
pub use serum3_cancel_all_orders::*;
|
||||
pub use serum3_cancel_order::*;
|
||||
pub use serum3_close_open_orders::*;
|
||||
|
@ -94,6 +96,7 @@ mod group_close;
|
|||
mod group_create;
|
||||
mod group_edit;
|
||||
mod group_withdraw_insurance_fund;
|
||||
mod health_check;
|
||||
mod health_region;
|
||||
mod ix_gate_set;
|
||||
mod openbook_v2_cancel_order;
|
||||
|
@ -123,6 +126,7 @@ mod perp_place_order;
|
|||
mod perp_settle_fees;
|
||||
mod perp_settle_pnl;
|
||||
mod perp_update_funding;
|
||||
mod sequence_check;
|
||||
mod serum3_cancel_all_orders;
|
||||
mod serum3_cancel_order;
|
||||
mod serum3_close_open_orders;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct SequenceCheck<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.is_ix_enabled(IxGate::SequenceCheck) @ MangoError::IxIsDisabled,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccountFixed>,
|
||||
pub owner: Signer<'info>,
|
||||
}
|
|
@ -13,6 +13,10 @@ pub trait AccountReader {
|
|||
fn data(&self) -> &[u8];
|
||||
}
|
||||
|
||||
pub trait AccountDataWriter {
|
||||
fn data_as_mut_slice(&mut self) -> &mut [u8];
|
||||
}
|
||||
|
||||
/// Like AccountReader, but can also get the account pubkey
|
||||
pub trait KeyedAccountReader: AccountReader {
|
||||
fn key(&self) -> &Pubkey;
|
||||
|
@ -99,6 +103,12 @@ impl<'info, 'a> KeyedAccountReader for AccountInfoRefMut<'info, 'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'info, 'a> AccountDataWriter for AccountInfoRefMut<'info, 'a> {
|
||||
fn data_as_mut_slice(&mut self) -> &mut [u8] {
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "solana-sdk")]
|
||||
impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
|
||||
fn owner(&self) -> &Pubkey {
|
||||
|
@ -110,6 +120,13 @@ impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "solana-sdk")]
|
||||
impl<T: solana_sdk::account::WritableAccount> AccountDataWriter for T {
|
||||
fn data_as_mut_slice(&mut self) -> &mut [u8] {
|
||||
self.data_as_mut_slice()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "solana-sdk")]
|
||||
#[derive(Clone)]
|
||||
pub struct KeyedAccount {
|
||||
|
@ -232,28 +249,29 @@ impl<A: AccountReader> LoadZeroCopy for A {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'info, 'a> LoadMutZeroCopy for AccountInfoRefMut<'info, 'a> {
|
||||
impl<A: AccountReader + AccountDataWriter> LoadMutZeroCopy for A {
|
||||
fn load_mut<T: ZeroCopy + Owner>(&mut self) -> Result<&mut T> {
|
||||
if self.owner != &T::owner() {
|
||||
if self.owner() != &T::owner() {
|
||||
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||
}
|
||||
|
||||
if self.data.len() < 8 {
|
||||
let data = self.data_as_mut_slice();
|
||||
if data.len() < 8 {
|
||||
return Err(ErrorCode::AccountDiscriminatorNotFound.into());
|
||||
}
|
||||
let disc_bytes = array_ref![self.data, 0, 8];
|
||||
let disc_bytes = array_ref![data, 0, 8];
|
||||
if disc_bytes != &T::discriminator() {
|
||||
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
|
||||
}
|
||||
|
||||
Ok(bytemuck::from_bytes_mut(
|
||||
&mut self.data[8..mem::size_of::<T>() + 8],
|
||||
&mut data[8..mem::size_of::<T>() + 8],
|
||||
))
|
||||
}
|
||||
|
||||
fn load_mut_fully_unchecked<T: ZeroCopy + Owner>(&mut self) -> Result<&mut T> {
|
||||
Ok(bytemuck::from_bytes_mut(
|
||||
&mut self.data[8..mem::size_of::<T>() + 8],
|
||||
&mut self.data_as_mut_slice()[8..mem::size_of::<T>() + 8],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,6 +145,12 @@ pub enum MangoError {
|
|||
MissingFeedForCLMMOracle,
|
||||
#[msg("the asset does not allow liquidation")]
|
||||
TokenAssetLiquidationDisabled,
|
||||
#[msg("for borrows the bank must be in the health account list")]
|
||||
BorrowsRequireHealthAccountBank,
|
||||
#[msg("invalid sequence number")]
|
||||
InvalidSequenceNumber,
|
||||
#[msg("invalid health")]
|
||||
InvalidHealth,
|
||||
}
|
||||
|
||||
impl MangoError {
|
||||
|
|
|
@ -26,6 +26,9 @@ use crate::state::{Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, TokenInde
|
|||
/// are passed because health needs to be computed for different baskets in
|
||||
/// one instruction (such as for liquidation instructions).
|
||||
pub trait AccountRetriever {
|
||||
/// Returns the token indexes of the available banks. Unordered and may have duplicates.
|
||||
fn available_banks(&self) -> Result<Vec<TokenIndex>>;
|
||||
|
||||
fn bank_and_oracle(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
|
@ -45,11 +48,12 @@ pub trait AccountRetriever {
|
|||
|
||||
/// Assumes the account infos needed for the health computation follow a strict order.
|
||||
///
|
||||
/// 1. n_banks Bank account, in the order of account.token_iter_active()
|
||||
/// 1. n_banks Bank account, in the order of account.active_token_positions() although it's
|
||||
/// allowed for some of the banks (and their oracles in 2.) to be skipped
|
||||
/// 2. n_banks oracle accounts, one for each bank in the same order
|
||||
/// 3. PerpMarket accounts, in the order of account.perps.iter_active_accounts()
|
||||
/// 3. PerpMarket accounts, in the order of account.perps.active_perp_positions()
|
||||
/// 4. PerpMarket oracle accounts, in the order of the perp market accounts
|
||||
/// 5. serum3 OpenOrders accounts, in the order of account.serum3.iter_active()
|
||||
/// 5. serum3 OpenOrders accounts, in the order of account.active_serum3_orders()
|
||||
/// 6. fallback oracle accounts, order and existence of accounts is not guaranteed
|
||||
pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
||||
pub ais: Vec<T>,
|
||||
|
@ -63,20 +67,67 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
|||
pub sol_oracle_index: Option<usize>,
|
||||
}
|
||||
|
||||
/// Creates a FixedOrderAccountRetriever where all banks are present
|
||||
///
|
||||
/// Note that this does not eagerly validate that the right accounts were passed. That
|
||||
/// validation happens only when banks, perps etc are requested.
|
||||
pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||
ais: &'a [AccountInfo<'info>],
|
||||
account: &MangoAccountRef,
|
||||
now_slot: u64,
|
||||
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
|
||||
let active_token_len = account.active_token_positions().count();
|
||||
|
||||
// Load the banks early to verify them
|
||||
for ai in &ais[0..active_token_len] {
|
||||
ai.load::<Bank>()?;
|
||||
}
|
||||
|
||||
new_fixed_order_account_retriever_inner(ais, account, now_slot, active_token_len)
|
||||
}
|
||||
|
||||
/// A FixedOrderAccountRetriever with n_banks <= active_token_positions().count(),
|
||||
/// depending on which banks were passed.
|
||||
///
|
||||
/// Note that this does not eagerly validate that the right accounts were passed. That
|
||||
/// validation happens only when banks, perps etc are requested.
|
||||
pub fn new_fixed_order_account_retriever_with_optional_banks<'a, 'info>(
|
||||
ais: &'a [AccountInfo<'info>],
|
||||
account: &MangoAccountRef,
|
||||
now_slot: u64,
|
||||
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
|
||||
// Scan for the number of banks provided
|
||||
let mut n_banks = 0;
|
||||
for ai in ais {
|
||||
if let Some((_, bank_result)) = can_load_as::<Bank>((0, ai)) {
|
||||
bank_result?;
|
||||
n_banks += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let active_token_len = account.active_token_positions().count();
|
||||
require_gte!(active_token_len, n_banks);
|
||||
|
||||
new_fixed_order_account_retriever_inner(ais, account, now_slot, n_banks)
|
||||
}
|
||||
|
||||
pub fn new_fixed_order_account_retriever_inner<'a, 'info>(
|
||||
ais: &'a [AccountInfo<'info>],
|
||||
account: &MangoAccountRef,
|
||||
now_slot: u64,
|
||||
n_banks: usize,
|
||||
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
|
||||
let active_serum3_len = account.active_serum3_orders().count();
|
||||
let active_perp_len = account.active_perp_positions().count();
|
||||
let expected_ais = active_token_len * 2 // banks + oracles
|
||||
let expected_ais = n_banks * 2 // banks + oracles
|
||||
+ active_perp_len * 2 // PerpMarkets + Oracles
|
||||
+ active_serum3_len; // open_orders
|
||||
require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount,
|
||||
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
|
||||
ais.len(), expected_ais,
|
||||
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len
|
||||
n_banks, n_banks, active_perp_len, active_perp_len, active_serum3_len
|
||||
);
|
||||
let usdc_oracle_index = ais[..]
|
||||
.iter()
|
||||
|
@ -87,11 +138,11 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
|
||||
Ok(FixedOrderAccountRetriever {
|
||||
ais: AccountInfoRef::borrow_slice(ais)?,
|
||||
n_banks: active_token_len,
|
||||
n_banks,
|
||||
n_perps: active_perp_len,
|
||||
begin_perp: active_token_len * 2,
|
||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||
staleness_slot: Some(Clock::get()?.slot),
|
||||
begin_perp: n_banks * 2,
|
||||
begin_serum3: n_banks * 2 + active_perp_len * 2,
|
||||
staleness_slot: Some(now_slot),
|
||||
begin_fallback_oracles: expected_ais,
|
||||
usdc_oracle_index,
|
||||
sol_oracle_index,
|
||||
|
@ -99,11 +150,28 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
}
|
||||
|
||||
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
||||
fn bank(&self, group: &Pubkey, account_index: usize, token_index: TokenIndex) -> Result<&Bank> {
|
||||
let bank = self.ais[account_index].load::<Bank>()?;
|
||||
require_keys_eq!(bank.group, *group);
|
||||
require_eq!(bank.token_index, token_index);
|
||||
Ok(bank)
|
||||
fn bank(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
active_token_position_index: usize,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<(usize, &Bank)> {
|
||||
// Maybe not all banks were passed: The desired bank must be at or
|
||||
// to the left of account_index and left of n_banks.
|
||||
let end_index = (active_token_position_index + 1).min(self.n_banks);
|
||||
for i in (0..end_index).rev() {
|
||||
let ai = &self.ais[i];
|
||||
let bank = ai.load_fully_unchecked::<Bank>()?;
|
||||
if bank.token_index == token_index {
|
||||
require_keys_eq!(bank.group, *group);
|
||||
return Ok((i, bank));
|
||||
}
|
||||
}
|
||||
Err(error_msg_typed!(
|
||||
MangoError::InvalidHealthAccountCount,
|
||||
"bank for token index {} not found",
|
||||
token_index
|
||||
))
|
||||
}
|
||||
|
||||
fn perp_market(
|
||||
|
@ -146,25 +214,25 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
|||
}
|
||||
|
||||
impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
|
||||
fn available_banks(&self) -> Result<Vec<TokenIndex>> {
|
||||
let mut result = Vec::with_capacity(self.n_banks);
|
||||
for bank_ai in &self.ais[0..self.n_banks] {
|
||||
let bank = bank_ai.load_fully_unchecked::<Bank>()?;
|
||||
result.push(bank.token_index);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn bank_and_oracle(
|
||||
&self,
|
||||
group: &Pubkey,
|
||||
active_token_position_index: usize,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<(&Bank, I80F48)> {
|
||||
let bank_account_index = active_token_position_index;
|
||||
let bank = self
|
||||
.bank(group, bank_account_index, token_index)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading bank with health account index {}, token index {}, passed account {}",
|
||||
bank_account_index,
|
||||
token_index,
|
||||
self.ais[bank_account_index].key(),
|
||||
)
|
||||
})?;
|
||||
let (bank_account_index, bank) =
|
||||
self.bank(group, active_token_position_index, token_index)?;
|
||||
|
||||
let oracle_index = self.n_banks + active_token_position_index;
|
||||
let oracle_index = self.n_banks + bank_account_index;
|
||||
let oracle_acc_infos = &self.create_oracle_infos(oracle_index, &bank.fallback_oracle);
|
||||
let oracle_price_result = bank.oracle_price(oracle_acc_infos, self.staleness_slot);
|
||||
let oracle_price = oracle_price_result.with_context(|| {
|
||||
|
@ -505,6 +573,10 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
}
|
||||
|
||||
impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
|
||||
fn available_banks(&self) -> Result<Vec<TokenIndex>> {
|
||||
Ok(self.banks_and_oracles.index_map.keys().copied().collect())
|
||||
}
|
||||
|
||||
fn bank_and_oracle(
|
||||
&self,
|
||||
_group: &Pubkey,
|
||||
|
@ -530,6 +602,8 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{MangoAccount, MangoAccountValue};
|
||||
|
||||
use super::super::test::*;
|
||||
use super::*;
|
||||
use serum_dex::state::OpenOrders;
|
||||
|
@ -650,4 +724,98 @@ mod tests {
|
|||
.perp_market_and_oracle_price(&group, 1, 5)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixed_account_retriever_with_skips() {
|
||||
let group = Pubkey::new_unique();
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 10, 1.0, 0.2, 0.1);
|
||||
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 20, 2.0, 0.2, 0.1);
|
||||
let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 30, 3.0, 0.2, 0.1);
|
||||
|
||||
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 2.0, 9, (0.2, 0.1), (0.05, 0.02));
|
||||
let mut oracle2_clone = oracle2.clone();
|
||||
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
account.ensure_token_position(10).unwrap();
|
||||
account.ensure_token_position(20).unwrap();
|
||||
account.ensure_token_position(30).unwrap();
|
||||
account.ensure_perp_position(9, 10).unwrap();
|
||||
|
||||
// pass all
|
||||
{
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank2.as_account_info(),
|
||||
bank3.as_account_info(),
|
||||
oracle1.as_account_info(),
|
||||
oracle2.as_account_info(),
|
||||
oracle3.as_account_info(),
|
||||
perp1.as_account_info(),
|
||||
oracle2_clone.as_account_info(),
|
||||
];
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
|
||||
.unwrap();
|
||||
assert_eq!(retriever.available_banks(), Ok(vec![10, 20, 30]));
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 0, 10).unwrap();
|
||||
assert_eq!(i, 0);
|
||||
assert_eq!(bank.token_index, 10);
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 1, 20).unwrap();
|
||||
assert_eq!(i, 1);
|
||||
assert_eq!(bank.token_index, 20);
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 2, 30).unwrap();
|
||||
assert_eq!(i, 2);
|
||||
assert_eq!(bank.token_index, 30);
|
||||
|
||||
assert!(retriever.perp_market(&group, 6, 9).is_ok());
|
||||
}
|
||||
|
||||
// skip bank2
|
||||
{
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank3.as_account_info(),
|
||||
oracle1.as_account_info(),
|
||||
oracle3.as_account_info(),
|
||||
perp1.as_account_info(),
|
||||
oracle2_clone.as_account_info(),
|
||||
];
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
|
||||
.unwrap();
|
||||
assert_eq!(retriever.available_banks(), Ok(vec![10, 30]));
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 0, 10).unwrap();
|
||||
assert_eq!(i, 0);
|
||||
assert_eq!(bank.token_index, 10);
|
||||
|
||||
let (i, bank) = retriever.bank(&group, 2, 30).unwrap();
|
||||
assert_eq!(i, 1);
|
||||
assert_eq!(bank.token_index, 30);
|
||||
|
||||
assert!(retriever.bank(&group, 1, 20).is_err());
|
||||
|
||||
assert!(retriever.perp_market(&group, 4, 9).is_ok());
|
||||
}
|
||||
|
||||
// skip all
|
||||
{
|
||||
let ais = vec![perp1.as_account_info(), oracle2_clone.as_account_info()];
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
|
||||
.unwrap();
|
||||
assert_eq!(retriever.available_banks(), Ok(vec![]));
|
||||
|
||||
assert!(retriever.bank(&group, 0, 10).is_err());
|
||||
assert!(retriever.bank(&group, 1, 20).is_err());
|
||||
assert!(retriever.bank(&group, 2, 30).is_err());
|
||||
|
||||
assert!(retriever.perp_market(&group, 0, 9).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ pub fn compute_health_from_fixed_accounts(
|
|||
ais: &[AccountInfo],
|
||||
now_ts: u64,
|
||||
) -> Result<I80F48> {
|
||||
let retriever = new_fixed_order_account_retriever(ais, account)?;
|
||||
let retriever = new_fixed_order_account_retriever(ais, account, Clock::get()?.slot)?;
|
||||
Ok(new_health_cache(account, &retriever, now_ts)?.health(health_type))
|
||||
}
|
||||
|
||||
|
@ -820,6 +820,12 @@ impl HealthCache {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn has_token_info(&self, token_index: TokenIndex) -> bool {
|
||||
self.token_infos
|
||||
.iter()
|
||||
.any(|t| t.token_index == token_index)
|
||||
}
|
||||
|
||||
pub fn perp_info(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpInfo> {
|
||||
Ok(&self.perp_infos[self.perp_info_index(perp_market_index)?])
|
||||
}
|
||||
|
@ -1234,11 +1240,11 @@ pub fn new_health_cache(
|
|||
}
|
||||
|
||||
/// Generate a special HealthCache for an account and its health accounts
|
||||
/// where nonnegative token positions for bad oracles are skipped.
|
||||
/// where nonnegative token positions for bad oracles are skipped as well as missing banks.
|
||||
///
|
||||
/// This health cache must be used carefully, since it doesn't provide the actual
|
||||
/// account health, just a value that is guaranteed to be less than it.
|
||||
pub fn new_health_cache_skipping_bad_oracles(
|
||||
pub fn new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
account: &MangoAccountRef,
|
||||
retriever: &impl AccountRetriever,
|
||||
now_ts: u64,
|
||||
|
@ -1246,22 +1252,49 @@ pub fn new_health_cache_skipping_bad_oracles(
|
|||
new_health_cache_impl(account, retriever, now_ts, true)
|
||||
}
|
||||
|
||||
// On `allow_skipping_banks`:
|
||||
// If (a Bank is not provided or its oracle is stale or inconfident) and the health contribution would
|
||||
// not be negative, skip it. This decreases health, but many operations are still allowed as long
|
||||
// as the decreased amount stays positive.
|
||||
fn new_health_cache_impl(
|
||||
account: &MangoAccountRef,
|
||||
retriever: &impl AccountRetriever,
|
||||
now_ts: u64,
|
||||
// If an oracle is stale or inconfident and the health contribution would
|
||||
// not be negative, skip it. This decreases health, but maybe overall it's
|
||||
// still positive?
|
||||
skip_bad_oracles: bool,
|
||||
allow_skipping_banks: bool,
|
||||
) -> Result<HealthCache> {
|
||||
// token contribution from token accounts
|
||||
let mut token_infos = Vec::with_capacity(account.active_token_positions().count());
|
||||
|
||||
// As a CU optimization, don't call available_banks() unless necessary
|
||||
let available_banks_opt = if allow_skipping_banks {
|
||||
Some(retriever.available_banks()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (i, position) in account.active_token_positions().enumerate() {
|
||||
// Allow skipping of missing banks only if the account has a nonnegative balance
|
||||
if allow_skipping_banks {
|
||||
let bank_is_available = available_banks_opt
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains(&position.token_index);
|
||||
if !bank_is_available {
|
||||
require_msg_typed!(
|
||||
position.indexed_position >= 0,
|
||||
MangoError::InvalidBank,
|
||||
"the bank for token index {} is a required health account when the account has a negative balance in it",
|
||||
position.token_index
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let bank_oracle_result =
|
||||
retriever.bank_and_oracle(&account.fixed.group, i, position.token_index);
|
||||
if skip_bad_oracles
|
||||
|
||||
// Allow skipping of bad-oracle banks if the account has a nonnegative balance
|
||||
if allow_skipping_banks
|
||||
&& bank_oracle_result.is_oracle_error()
|
||||
&& position.indexed_position >= 0
|
||||
{
|
||||
|
@ -1301,9 +1334,25 @@ fn new_health_cache_impl(
|
|||
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
|
||||
|
||||
// find the TokenInfos for the market's base and quote tokens
|
||||
let base_info_index = find_token_info_index(&token_infos, serum_account.base_token_index)?;
|
||||
let quote_info_index =
|
||||
find_token_info_index(&token_infos, serum_account.quote_token_index)?;
|
||||
// and potentially skip the whole serum contribution if they are not available
|
||||
let info_index_results = (
|
||||
find_token_info_index(&token_infos, serum_account.base_token_index),
|
||||
find_token_info_index(&token_infos, serum_account.quote_token_index),
|
||||
);
|
||||
let (base_info_index, quote_info_index) = match info_index_results {
|
||||
(Ok(base), Ok(quote)) => (base, quote),
|
||||
_ => {
|
||||
require_msg_typed!(
|
||||
allow_skipping_banks,
|
||||
MangoError::InvalidBank,
|
||||
"serum market {} misses health accounts for bank {} or {}",
|
||||
serum_account.market_index,
|
||||
serum_account.base_token_index,
|
||||
serum_account.quote_token_index,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// add the amounts that are freely settleable immediately to token balances
|
||||
let base_free = I80F48::from(oo.native_coin_free);
|
||||
|
@ -1329,6 +1378,12 @@ fn new_health_cache_impl(
|
|||
i,
|
||||
perp_position.market_index,
|
||||
)?;
|
||||
|
||||
// Ensure the settle token is available in the health cache
|
||||
if allow_skipping_banks {
|
||||
find_token_info_index(&token_infos, perp_market.settle_token_index)?;
|
||||
}
|
||||
|
||||
perp_infos.push(PerpInfo::new(
|
||||
perp_position,
|
||||
perp_market,
|
||||
|
@ -1879,4 +1934,170 @@ mod tests {
|
|||
test_health1_runner(testcase);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_with_skips() {
|
||||
let testcase = TestHealth1Case {
|
||||
// 6, reserved oo funds
|
||||
token1: 100,
|
||||
token2: 10,
|
||||
token3: -10,
|
||||
oo_1_2: (5, 1),
|
||||
oo_1_3: (0, 0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
|
||||
let group = Pubkey::new_unique();
|
||||
account.fixed.group = group;
|
||||
|
||||
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
|
||||
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
|
||||
let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3);
|
||||
bank1
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account.ensure_token_position(0).unwrap().0,
|
||||
I80F48::from(testcase.token1),
|
||||
DUMMY_NOW_TS,
|
||||
)
|
||||
.unwrap();
|
||||
bank2
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account.ensure_token_position(4).unwrap().0,
|
||||
I80F48::from(testcase.token2),
|
||||
DUMMY_NOW_TS,
|
||||
)
|
||||
.unwrap();
|
||||
bank3
|
||||
.data()
|
||||
.change_without_fee(
|
||||
account.ensure_token_position(5).unwrap().0,
|
||||
I80F48::from(testcase.token3),
|
||||
DUMMY_NOW_TS,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
|
||||
let serum3account1 = account.create_serum3_orders(2).unwrap();
|
||||
serum3account1.open_orders = oo1.pubkey;
|
||||
serum3account1.base_token_index = 4;
|
||||
serum3account1.quote_token_index = 0;
|
||||
oo1.data().native_pc_total = testcase.oo_1_2.0;
|
||||
oo1.data().native_coin_total = testcase.oo_1_2.1;
|
||||
|
||||
fn compute_health_with_retriever<'a, 'info>(
|
||||
ais: &[AccountInfo],
|
||||
account: &MangoAccountValue,
|
||||
group: Pubkey,
|
||||
kind: bool,
|
||||
) -> Result<I80F48> {
|
||||
let hc = if kind {
|
||||
let retriever =
|
||||
ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
DUMMY_NOW_TS,
|
||||
)?
|
||||
} else {
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
&ais,
|
||||
&account.borrow(),
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
DUMMY_NOW_TS,
|
||||
)?
|
||||
};
|
||||
Ok(hc.health(HealthType::Init))
|
||||
}
|
||||
|
||||
for retriever_kind in [false, true] {
|
||||
// baseline with everything
|
||||
{
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank2.as_account_info(),
|
||||
bank3.as_account_info(),
|
||||
oracle1.as_account_info(),
|
||||
oracle2.as_account_info(),
|
||||
oracle3.as_account_info(),
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
let health =
|
||||
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
|
||||
assert!(health_eq(
|
||||
health,
|
||||
0.8 * 100.0 + 0.5 * 5.0 * (10.0 + 2.0) - 1.5 * 10.0 * 10.0
|
||||
));
|
||||
}
|
||||
|
||||
// missing bank1
|
||||
{
|
||||
let ais = vec![
|
||||
bank2.as_account_info(),
|
||||
bank3.as_account_info(),
|
||||
oracle2.as_account_info(),
|
||||
oracle3.as_account_info(),
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
let health =
|
||||
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
|
||||
assert!(health_eq(health, 0.5 * 5.0 * 10.0 - 1.5 * 10.0 * 10.0));
|
||||
}
|
||||
|
||||
// missing bank2
|
||||
{
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank3.as_account_info(),
|
||||
oracle1.as_account_info(),
|
||||
oracle3.as_account_info(),
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
let health =
|
||||
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
|
||||
assert!(health_eq(health, 0.8 * 100.0 - 1.5 * 10.0 * 10.0));
|
||||
}
|
||||
|
||||
// missing bank1 and 2
|
||||
{
|
||||
let ais = vec![
|
||||
bank3.as_account_info(),
|
||||
oracle3.as_account_info(),
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
let health =
|
||||
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
|
||||
assert!(health_eq(health, -1.5 * 10.0 * 10.0));
|
||||
}
|
||||
|
||||
// missing bank3
|
||||
{
|
||||
let ais = vec![
|
||||
bank1.as_account_info(),
|
||||
bank2.as_account_info(),
|
||||
oracle1.as_account_info(),
|
||||
oracle2.as_account_info(),
|
||||
oo1.as_account_info(),
|
||||
];
|
||||
|
||||
// bank3 has a negative balance and can't be skipped!
|
||||
assert!(
|
||||
compute_health_with_retriever(&ais, &account, group, retriever_kind).is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ use crate::accounts_ix::*;
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::group_seeds;
|
||||
use crate::health::{new_fixed_order_account_retriever, new_health_cache, AccountRetriever};
|
||||
use crate::health::*;
|
||||
use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog};
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
||||
|
@ -368,8 +369,9 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
// all vaults must have had matching banks
|
||||
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
|
||||
require_msg!(
|
||||
require_msg_typed!(
|
||||
has_bank,
|
||||
MangoError::InvalidBank,
|
||||
"missing bank for vault index {}, address {}",
|
||||
i,
|
||||
vaults[i].key
|
||||
|
@ -387,12 +389,26 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
}
|
||||
|
||||
// Check health before balance adjustments
|
||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
|
||||
// The vault-to-bank matching above ensures that the banks for the affected tokens are available.
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
health_ais,
|
||||
&account.borrow(),
|
||||
now_slot,
|
||||
)?;
|
||||
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)?;
|
||||
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
|
||||
// Prices for logging and net borrow checks
|
||||
//
|
||||
// This also verifies that all affected banks/oracles are available in health_cache:
|
||||
// That is essential to avoid issues around withdrawing tokens when init health is negative
|
||||
// (similar issue to token_withdraw)
|
||||
let mut oracle_prices = vec![];
|
||||
for change in &changes {
|
||||
let (_, oracle_price) = retriever.bank_and_oracle(
|
||||
|
@ -400,6 +416,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
change.bank_index,
|
||||
change.token_index,
|
||||
)?;
|
||||
// Sanity check
|
||||
health_cache.token_info_index(change.token_index)?;
|
||||
|
||||
oracle_prices.push(oracle_price);
|
||||
}
|
||||
|
@ -502,8 +520,16 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
});
|
||||
|
||||
// Check health after account position changes
|
||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
health_ais,
|
||||
&account.borrow(),
|
||||
now_slot,
|
||||
)?;
|
||||
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)?;
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
|
||||
// Deactivate inactive token accounts after health check
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::error::{Contextable, MangoError};
|
||||
use crate::health::{
|
||||
new_health_cache_skipping_missing_banks_and_bad_oracles, HealthType, ScanningAccountRetriever,
|
||||
};
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
|
||||
pub fn health_check(
|
||||
ctx: Context<HealthCheck>,
|
||||
min_value: f64,
|
||||
health_check_kind: HealthCheckKind,
|
||||
) -> Result<()> {
|
||||
let account = ctx.accounts.account.load_full_mut()?;
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
|
||||
let group_pk = &ctx.accounts.group.key();
|
||||
|
||||
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||
.context("create account retriever")?;
|
||||
|
||||
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("health_check health cache")?;
|
||||
|
||||
let min_value = I80F48::from_num(min_value);
|
||||
let actual_value = match health_check_kind {
|
||||
HealthCheckKind::Maint => health_cache.health(HealthType::Maint),
|
||||
HealthCheckKind::Init => health_cache.health(HealthType::Init),
|
||||
HealthCheckKind::LiquidationEnd => health_cache.health(HealthType::LiquidationEnd),
|
||||
HealthCheckKind::MaintRatio => health_cache.health_ratio(HealthType::Maint),
|
||||
HealthCheckKind::InitRatio => health_cache.health_ratio(HealthType::Init),
|
||||
HealthCheckKind::LiquidationEndRatio => {
|
||||
health_cache.health_ratio(HealthType::LiquidationEnd)
|
||||
}
|
||||
};
|
||||
|
||||
// msg!("{}", actual_value);
|
||||
require_gte!(actual_value, min_value, MangoError::InvalidHealth);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -96,6 +96,8 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
|
|||
);
|
||||
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
|
||||
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
|
||||
log_if_changed(&group, ix_gate, IxGate::SequenceCheck);
|
||||
log_if_changed(&group, ix_gate, IxGate::HealthCheck);
|
||||
|
||||
group.ix_gate = ix_gate;
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ pub use group_close::*;
|
|||
pub use group_create::*;
|
||||
pub use group_edit::*;
|
||||
pub use group_withdraw_insurance_fund::*;
|
||||
pub use health_check::*;
|
||||
pub use health_region::*;
|
||||
pub use ix_gate_set::*;
|
||||
pub use perp_cancel_all_orders::*;
|
||||
|
@ -35,6 +36,7 @@ pub use perp_place_order::*;
|
|||
pub use perp_settle_fees::*;
|
||||
pub use perp_settle_pnl::*;
|
||||
pub use perp_update_funding::*;
|
||||
pub use sequence_check::*;
|
||||
pub use serum3_cancel_all_orders::*;
|
||||
pub use serum3_cancel_order::*;
|
||||
pub use serum3_cancel_order_by_client_order_id::*;
|
||||
|
@ -85,6 +87,7 @@ mod group_close;
|
|||
mod group_create;
|
||||
mod group_edit;
|
||||
mod group_withdraw_insurance_fund;
|
||||
mod health_check;
|
||||
mod health_region;
|
||||
mod ix_gate_set;
|
||||
mod perp_cancel_all_orders;
|
||||
|
@ -104,6 +107,7 @@ mod perp_place_order;
|
|||
mod perp_settle_fees;
|
||||
mod perp_settle_pnl;
|
||||
mod perp_update_funding;
|
||||
mod sequence_check;
|
||||
mod serum3_cancel_all_orders;
|
||||
mod serum3_cancel_order;
|
||||
mod serum3_cancel_order_by_client_order_id;
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::accounts_ix::*;
|
|||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
|
||||
pub fn perp_liq_force_cancel_orders(
|
||||
ctx: Context<PerpLiqForceCancelOrders>,
|
||||
|
@ -11,10 +12,10 @@ pub fn perp_liq_force_cancel_orders(
|
|||
) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
let mut health_cache = {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
|
||||
new_health_cache(&account.borrow(), &retriever, now_ts).context("create health cache")?
|
||||
};
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@ use anchor_lang::prelude::*;
|
|||
use crate::accounts_ix::*;
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::health::{new_fixed_order_account_retriever, new_health_cache};
|
||||
use crate::health::*;
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
|
||||
// TODO
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -16,7 +17,7 @@ pub fn perp_place_order(
|
|||
require_gte!(order.max_base_lots, 0);
|
||||
require_gte!(order.max_quote_lots, 0);
|
||||
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
let oracle_price;
|
||||
|
||||
// Update funding if possible.
|
||||
|
@ -66,10 +67,21 @@ pub fn perp_place_order(
|
|||
// Pre-health computation, _after_ perp position is created
|
||||
//
|
||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("pre-withdraw init health")?;
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
ctx.remaining_accounts,
|
||||
&account.borrow(),
|
||||
now_slot,
|
||||
)?;
|
||||
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("pre init health")?;
|
||||
|
||||
// The settle token banks/oracles must be passed and be valid
|
||||
health_cache.token_info_index(settle_token_index)?;
|
||||
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
} else {
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::state::*;
|
|||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog, TokenBalanceLog};
|
||||
use crate::util::clock_now;
|
||||
|
||||
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
|
||||
// max_settle_amount must greater than zero
|
||||
|
@ -123,8 +124,9 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
|||
drop(perp_market);
|
||||
|
||||
// Verify that the result of settling did not violate the health of the account that lost money
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
|
||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever, now_ts)?;
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::error::MangoError;
|
||||
use crate::state::*;
|
||||
|
||||
pub fn sequence_check(ctx: Context<SequenceCheck>, expected_sequence_number: u8) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
|
||||
require_eq!(
|
||||
expected_sequence_number,
|
||||
account.fixed.sequence_number,
|
||||
MangoError::InvalidSequenceNumber
|
||||
);
|
||||
|
||||
account.fixed.sequence_number = account.fixed.sequence_number.wrapping_add(1);
|
||||
Ok(())
|
||||
}
|
|
@ -8,6 +8,7 @@ use crate::instructions::charge_loan_origination_fees;
|
|||
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
|
||||
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
|
||||
pub fn serum3_liq_force_cancel_orders(
|
||||
ctx: Context<Serum3LiqForceCancelOrders>,
|
||||
|
@ -50,14 +51,15 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
);
|
||||
}
|
||||
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
|
||||
//
|
||||
// Early return if if liquidation is not allowed or if market is not in force close
|
||||
//
|
||||
let mut health_cache = {
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("create health cache")?;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
|
|||
use crate::serum3_cpi::{
|
||||
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
|
||||
};
|
||||
use crate::util::clock_now;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
|
@ -40,6 +41,7 @@ pub fn serum3_place_order(
|
|||
// Validation
|
||||
//
|
||||
let receiver_token_index;
|
||||
let payer_token_index;
|
||||
{
|
||||
let account = ctx.accounts.account.load_full()?;
|
||||
// account constraint #1
|
||||
|
@ -60,7 +62,7 @@ pub fn serum3_place_order(
|
|||
// Validate bank and vault #3
|
||||
let payer_bank = ctx.accounts.payer_bank.load()?;
|
||||
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
|
||||
let payer_token_index = match side {
|
||||
payer_token_index = match side {
|
||||
Serum3Side::Bid => serum_market.quote_token_index,
|
||||
Serum3Side::Ask => serum_market.base_token_index,
|
||||
};
|
||||
|
@ -76,10 +78,23 @@ pub fn serum3_place_order(
|
|||
// Pre-health computation
|
||||
//
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let mut health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("pre-withdraw init health")?;
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
ctx.remaining_accounts,
|
||||
&account.borrow(),
|
||||
now_slot,
|
||||
)?;
|
||||
let mut health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("pre init health")?;
|
||||
|
||||
// The payer and receiver token banks/oracles must be passed and be valid
|
||||
health_cache.token_info_index(payer_token_index)?;
|
||||
health_cache.token_info_index(receiver_token_index)?;
|
||||
|
||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some(pre_init_health)
|
||||
|
@ -412,6 +427,20 @@ pub fn serum3_place_order(
|
|||
// Note that all orders on the book executing can still cause a net deposit. That's because
|
||||
// the total serum3 potential amount assumes all reserved amounts convert at the current
|
||||
// oracle price.
|
||||
//
|
||||
// This also requires that all serum3 oos that touch the receiver_token are avaliable in the
|
||||
// health cache. We make this a general requirement to avoid surprises.
|
||||
for serum3 in account.active_serum3_orders() {
|
||||
if serum3.base_token_index == receiver_token_index
|
||||
|| serum3.quote_token_index == receiver_token_index
|
||||
{
|
||||
require_msg!(
|
||||
health_cache.serum3_infos.iter().any(|s3| s3.market_index == serum3.market_index),
|
||||
"health cache is missing serum3 info {} involving receiver token {}; passed banks and oracles?",
|
||||
serum3.market_index, receiver_token_index
|
||||
);
|
||||
}
|
||||
}
|
||||
if receiver_bank_reduce_only {
|
||||
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
|
||||
let potential =
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::health::*;
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
|
@ -10,7 +11,7 @@ use crate::logs::{emit_stack, TokenBalanceLog, TokenCollateralFeeLog};
|
|||
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
|
||||
if group.collateral_fee_interval == 0 {
|
||||
// By resetting, a new enabling of collateral fees will not immediately create a charge
|
||||
|
@ -42,7 +43,7 @@ pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) ->
|
|||
|
||||
let health_cache = {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
|
||||
new_health_cache(&account.borrow(), &retriever, now_ts)?
|
||||
};
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::state::*;
|
|||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::*;
|
||||
use crate::util::clock_now;
|
||||
|
||||
struct DepositCommon<'a, 'info> {
|
||||
pub group: &'a AccountLoader<'info, Group>,
|
||||
|
@ -119,13 +120,21 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
|||
//
|
||||
// Health computation
|
||||
//
|
||||
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
remaining_accounts,
|
||||
&account.borrow(),
|
||||
now_slot,
|
||||
)?;
|
||||
|
||||
// We only compute health to check if the account leaves the being_liquidated state.
|
||||
// So it's ok to possibly skip token positions for bad oracles and compute a health
|
||||
// So it's ok to possibly skip nonnegative token positions and compute a health
|
||||
// value that is too low.
|
||||
let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)?;
|
||||
let cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)?;
|
||||
|
||||
// Since depositing can only increase health, we can skip the usual pre-health computation.
|
||||
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
|
||||
|
@ -143,6 +152,13 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
|||
// Group level deposit limit on account
|
||||
let group = self.group.load()?;
|
||||
if group.deposit_limit_quote > 0 {
|
||||
// Requires that all banks were provided an all oracles are healthy, otherwise we
|
||||
// can't know how much this account has deposited
|
||||
require_eq!(
|
||||
cache.token_infos.len(),
|
||||
account.active_token_positions().count()
|
||||
);
|
||||
|
||||
let assets = cache
|
||||
.health_assets_and_liabs_stable_assets(HealthType::Init)
|
||||
.0
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::accounts_zerocopy::*;
|
|||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::state::*;
|
||||
use crate::util::clock_now;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::associated_token;
|
||||
use anchor_spl::token;
|
||||
|
@ -19,7 +20,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let (now_ts, now_slot) = clock_now();
|
||||
|
||||
// Create the account's position for that token index
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
|
@ -27,21 +28,19 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
|
||||
// Health check _after_ the token position is guaranteed to exist
|
||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let hc_result = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("pre-withdraw health cache");
|
||||
if hc_result.is_oracle_error() {
|
||||
// We allow NOT checking the pre init health. That means later on the health
|
||||
// check will be stricter (post_init > 0, without the post_init >= pre_init option)
|
||||
// Then later we can compute the health while ignoring potential nonnegative
|
||||
// health contributions from tokens with stale oracles.
|
||||
None
|
||||
} else {
|
||||
let health_cache = hc_result?;
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
}
|
||||
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||
ctx.remaining_accounts,
|
||||
&account.borrow(),
|
||||
now_slot,
|
||||
)?;
|
||||
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||
&account.borrow(),
|
||||
&retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("pre-withdraw health cache")?;
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -156,26 +155,29 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
//
|
||||
// Health check
|
||||
//
|
||||
if !account.fixed.is_in_health_region() {
|
||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
||||
// This is the normal case
|
||||
if let Some((mut health_cache, pre_init_health_lower_bound)) = pre_health_opt {
|
||||
if health_cache.has_token_info(token_index) {
|
||||
// This is the normal case: the health cache knows about the token, we can
|
||||
// compute the health for the new state by adjusting its balance
|
||||
health_cache.adjust_token_balance(&bank, native_position_after - native_position)?;
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
account.check_health_post(&health_cache, pre_init_health_lower_bound)?;
|
||||
} else {
|
||||
// Some oracle was stale/not confident enough above.
|
||||
// The health cache does not know about the token! It has a bad oracle or wasn't
|
||||
// provided in the health accounts. Borrows are out of the question!
|
||||
require!(!is_borrow, MangoError::BorrowsRequireHealthAccountBank);
|
||||
|
||||
// Since the health cache isn't aware of the bank we changed, the health
|
||||
// estimation is the same.
|
||||
let post_init_health_lower_bound = pre_init_health_lower_bound;
|
||||
|
||||
// If health without the token is positive, then full health is positive and
|
||||
// withdrawing all of the token would still keep it positive.
|
||||
// However, if health without it is negative then full health could be negative
|
||||
// and could be made worse by withdrawals.
|
||||
//
|
||||
// Try computing health while ignoring nonnegative contributions from bad oracles.
|
||||
// If the health is good enough without those, we can pass.
|
||||
//
|
||||
// Note that this must include the normal pre and post health checks.
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)
|
||||
.context("special post-withdraw health-cache")?;
|
||||
let post_init_health = health_cache.health(HealthType::Init);
|
||||
account.check_health_pre_checks(&health_cache, post_init_health)?;
|
||||
account.check_health_post_checks(I80F48::MAX, post_init_health)?;
|
||||
// We don't know the true pre_init_health: So require that our lower bound on
|
||||
// post health is strictly good enough.
|
||||
account.check_health_post_checks_strict(post_init_health_lower_bound)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -458,6 +458,22 @@ pub mod mango_v4 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sequence_check(ctx: Context<SequenceCheck>, expected_sequence_number: u8) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::sequence_check(ctx, expected_sequence_number)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn health_check(
|
||||
ctx: Context<HealthCheck>,
|
||||
min_health_value: f64,
|
||||
check_kind: HealthCheckKind,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::health_check(ctx, min_health_value, check_kind)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// todo:
|
||||
// ckamm: generally, using an I80F48 arg will make it harder to call
|
||||
// because generic anchor clients won't know how to deal with it
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use fixed::types::{I80F48, U64F64};
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use crate::{accounts_zerocopy::KeyedAccountReader, error::MangoError};
|
||||
|
||||
use super::{
|
||||
get_pyth_state, pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, sol_mint_mainnet,
|
||||
usdc_mint_mainnet, OracleAccountInfos, OracleState, QUOTE_DECIMALS, SOL_DECIMALS,
|
||||
};
|
||||
|
||||
pub mod orca_mainnet_whirlpool {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");
|
||||
}
|
||||
|
||||
pub mod raydium_mainnet {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK");
|
||||
}
|
||||
|
||||
pub const ORCA_WHIRLPOOL_LEN: usize = 653;
|
||||
pub const ORCA_WHIRLPOOL_DISCRIMINATOR: [u8; 8] = [63, 149, 209, 12, 225, 128, 99, 9];
|
||||
|
||||
pub const RAYDIUM_POOL_LEN: usize = 1544;
|
||||
pub const RAYDIUM_POOL_DISCRIMINATOR: [u8; 8] = [247, 237, 227, 245, 215, 195, 222, 70];
|
||||
|
||||
pub struct CLMMPoolState {
|
||||
// Q64.64
|
||||
pub sqrt_price: u128, // 16
|
||||
pub token_mint_a: Pubkey, // 32
|
||||
pub token_mint_b: Pubkey, // 32
|
||||
}
|
||||
|
||||
impl CLMMPoolState {
|
||||
pub fn is_inverted(&self) -> bool {
|
||||
self.token_mint_a == usdc_mint_mainnet::ID
|
||||
|| (self.token_mint_a == sol_mint_mainnet::ID
|
||||
&& self.token_mint_b != usdc_mint_mainnet::ID)
|
||||
}
|
||||
|
||||
pub fn get_clmm_price(&self) -> I80F48 {
|
||||
if self.is_inverted() {
|
||||
let sqrt_price = U64F64::from_bits(self.sqrt_price).to_num::<f64>();
|
||||
let inverted_price = sqrt_price * sqrt_price;
|
||||
I80F48::from_num(1.0f64 / inverted_price)
|
||||
} else {
|
||||
let sqrt_price = U64F64::from_bits(self.sqrt_price);
|
||||
I80F48::from_num(sqrt_price * sqrt_price)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quote_state_unchecked<T: KeyedAccountReader>(
|
||||
&self,
|
||||
acc_infos: &OracleAccountInfos<T>,
|
||||
) -> Result<OracleState> {
|
||||
if self.is_inverted() {
|
||||
self.quote_state_inner(acc_infos, &self.token_mint_a)
|
||||
} else {
|
||||
self.quote_state_inner(acc_infos, &self.token_mint_b)
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_state_inner<T: KeyedAccountReader>(
|
||||
&self,
|
||||
acc_infos: &OracleAccountInfos<T>,
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<OracleState> {
|
||||
if quote_mint == &usdc_mint_mainnet::ID {
|
||||
let usd_feed = acc_infos
|
||||
.usdc_opt
|
||||
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
|
||||
let usd_state = get_pyth_state(usd_feed, QUOTE_DECIMALS as u8)?;
|
||||
return Ok(usd_state);
|
||||
} else if quote_mint == &sol_mint_mainnet::ID {
|
||||
let sol_feed = acc_infos
|
||||
.sol_opt
|
||||
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
|
||||
let sol_state = get_pyth_state(sol_feed, SOL_DECIMALS as u8)?;
|
||||
return Ok(sol_state);
|
||||
} else {
|
||||
return Err(MangoError::MissingFeedForCLMMOracle.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_quote_oracle(&self) -> Result<Pubkey> {
|
||||
let mint = if self.is_inverted() {
|
||||
self.token_mint_a
|
||||
} else {
|
||||
self.token_mint_b
|
||||
};
|
||||
|
||||
if mint == usdc_mint_mainnet::ID {
|
||||
return Ok(pyth_mainnet_usdc_oracle::ID);
|
||||
} else if mint == sol_mint_mainnet::ID {
|
||||
return Ok(pyth_mainnet_sol_oracle::ID);
|
||||
} else {
|
||||
return Err(MangoError::MissingFeedForCLMMOracle.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_quote_token(&self) -> bool {
|
||||
let has_usdc_token = self.token_mint_a == usdc_mint_mainnet::ID
|
||||
|| self.token_mint_b == usdc_mint_mainnet::ID;
|
||||
let has_sol_token =
|
||||
self.token_mint_a == sol_mint_mainnet::ID || self.token_mint_b == sol_mint_mainnet::ID;
|
||||
|
||||
has_usdc_token || has_sol_token
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_orca_pool_state(acc_info: &impl KeyedAccountReader) -> Result<CLMMPoolState> {
|
||||
let data = &acc_info.data();
|
||||
require!(
|
||||
data[0..8] == ORCA_WHIRLPOOL_DISCRIMINATOR[..],
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
require!(
|
||||
data.len() == ORCA_WHIRLPOOL_LEN,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
require!(
|
||||
acc_info.owner() == &orca_mainnet_whirlpool::ID,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
|
||||
let price_bytes: &[u8; 16] = &data[65..81].try_into().unwrap();
|
||||
let sqrt_price = u128::from_le_bytes(*price_bytes);
|
||||
let a: &[u8; 32] = &(&data[101..133]).try_into().unwrap();
|
||||
let b: &[u8; 32] = &(&data[181..213]).try_into().unwrap();
|
||||
let mint_a = Pubkey::from(*a);
|
||||
let mint_b = Pubkey::from(*b);
|
||||
|
||||
Ok(CLMMPoolState {
|
||||
sqrt_price,
|
||||
token_mint_a: mint_a,
|
||||
token_mint_b: mint_b,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_raydium_pool_state(acc_info: &impl KeyedAccountReader) -> Result<CLMMPoolState> {
|
||||
let data = &acc_info.data();
|
||||
require!(
|
||||
data[0..8] == RAYDIUM_POOL_DISCRIMINATOR[..],
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
require!(
|
||||
data.len() == RAYDIUM_POOL_LEN,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
require!(
|
||||
acc_info.owner() == &raydium_mainnet::ID,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
|
||||
let price_bytes: &[u8; 16] = &data[253..269].try_into().unwrap();
|
||||
let sqrt_price = u128::from_le_bytes(*price_bytes);
|
||||
let a: &[u8; 32] = &(&data[73..105]).try_into().unwrap();
|
||||
let b: &[u8; 32] = &(&data[105..137]).try_into().unwrap();
|
||||
let mint_a = Pubkey::from(*a);
|
||||
let mint_b = Pubkey::from(*b);
|
||||
|
||||
Ok(CLMMPoolState {
|
||||
sqrt_price,
|
||||
token_mint_a: mint_a,
|
||||
token_mint_b: mint_b,
|
||||
})
|
||||
}
|
|
@ -246,6 +246,8 @@ pub enum IxGate {
|
|||
TokenConditionalSwapCreateLinearAuction = 70,
|
||||
Serum3PlaceOrderV2 = 71,
|
||||
TokenForceWithdraw = 72,
|
||||
SequenceCheck = 73,
|
||||
HealthCheck = 74,
|
||||
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
||||
}
|
||||
|
||||
|
|
|
@ -123,8 +123,7 @@ pub struct MangoAccount {
|
|||
|
||||
pub bump: u8,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub padding: [u8; 1],
|
||||
pub sequence_number: u8,
|
||||
|
||||
// (Display only)
|
||||
// Cumulative (deposits - withdraws)
|
||||
|
@ -200,7 +199,7 @@ impl MangoAccount {
|
|||
in_health_region: 0,
|
||||
account_num: 0,
|
||||
bump: 0,
|
||||
padding: Default::default(),
|
||||
sequence_number: 0,
|
||||
net_deposits: 0,
|
||||
perp_spot_transfers: 0,
|
||||
health_region_begin_init_health: 0,
|
||||
|
@ -325,7 +324,7 @@ pub struct MangoAccountFixed {
|
|||
being_liquidated: u8,
|
||||
in_health_region: u8,
|
||||
pub bump: u8,
|
||||
pub padding: [u8; 1],
|
||||
pub sequence_number: u8,
|
||||
pub net_deposits: i64,
|
||||
pub perp_spot_transfers: i64,
|
||||
pub health_region_begin_init_health: i64,
|
||||
|
@ -1458,6 +1457,13 @@ impl<
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// A stricter version of check_health_post_checks() that requires >=0 health, it not getting
|
||||
/// worse is not sufficient
|
||||
pub fn check_health_post_checks_strict(&mut self, post_init_health: I80F48) -> Result<()> {
|
||||
require!(post_init_health >= 0, MangoError::HealthMustBePositive);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
|
||||
// Once maint_health falls below 0, we want to start liquidating,
|
||||
// we want to allow liquidation to continue until init_health is positive,
|
||||
|
@ -2897,7 +2903,7 @@ mod tests {
|
|||
being_liquidated: fixed.being_liquidated,
|
||||
in_health_region: fixed.in_health_region,
|
||||
bump: fixed.bump,
|
||||
padding: Default::default(),
|
||||
sequence_number: 0,
|
||||
net_deposits: fixed.net_deposits,
|
||||
perp_spot_transfers: fixed.perp_spot_transfers,
|
||||
health_region_begin_init_health: fixed.health_region_begin_init_health,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub use amm_cpi::*;
|
||||
pub use bank::*;
|
||||
pub use dynamic_account::*;
|
||||
pub use equity::*;
|
||||
|
@ -7,13 +8,13 @@ pub use mango_account_components::*;
|
|||
pub use mint_info::*;
|
||||
pub use openbook_v2_market::*;
|
||||
pub use oracle::*;
|
||||
pub use orca_cpi::*;
|
||||
pub use orderbook::*;
|
||||
pub use perp_market::*;
|
||||
pub use serum3_market::*;
|
||||
pub use stable_price::*;
|
||||
pub use token_conditional_swap::*;
|
||||
|
||||
mod amm_cpi;
|
||||
mod bank;
|
||||
mod dynamic_account;
|
||||
mod equity;
|
||||
|
@ -23,7 +24,6 @@ mod mango_account_components;
|
|||
mod mint_info;
|
||||
mod openbook_v2_market;
|
||||
mod oracle;
|
||||
mod orca_cpi;
|
||||
mod orderbook;
|
||||
mod perp_market;
|
||||
mod serum3_market;
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::mem::size_of;
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::{AnchorDeserialize, Discriminator};
|
||||
use derivative::Derivative;
|
||||
use fixed::types::{I80F48, U64F64};
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use static_assertions::const_assert_eq;
|
||||
use switchboard_program::FastRoundResultAccountData;
|
||||
|
@ -12,9 +12,9 @@ use switchboard_v2::AggregatorAccountData;
|
|||
use crate::accounts_zerocopy::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::load_whirlpool_state;
|
||||
use crate::state::load_orca_pool_state;
|
||||
|
||||
use super::orca_mainnet_whirlpool;
|
||||
use super::{load_raydium_pool_state, orca_mainnet_whirlpool, raydium_mainnet};
|
||||
|
||||
const DECIMAL_CONSTANT_ZERO_INDEX: i8 = 12;
|
||||
const DECIMAL_CONSTANTS: [I80F48; 25] = [
|
||||
|
@ -117,6 +117,7 @@ pub enum OracleType {
|
|||
SwitchboardV1,
|
||||
SwitchboardV2,
|
||||
OrcaCLMM,
|
||||
RaydiumCLMM,
|
||||
}
|
||||
|
||||
pub struct OracleState {
|
||||
|
@ -195,6 +196,8 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|
|||
return Ok(OracleType::SwitchboardV1);
|
||||
} else if acc_info.owner() == &orca_mainnet_whirlpool::ID {
|
||||
return Ok(OracleType::OrcaCLMM);
|
||||
} else if acc_info.owner() == &raydium_mainnet::ID {
|
||||
return Ok(OracleType::RaydiumCLMM);
|
||||
}
|
||||
|
||||
Err(MangoError::UnknownOracleType.into())
|
||||
|
@ -205,18 +208,19 @@ pub fn check_is_valid_fallback_oracle(acc_info: &impl KeyedAccountReader) -> Res
|
|||
return Ok(());
|
||||
};
|
||||
let oracle_type = determine_oracle_type(acc_info)?;
|
||||
if oracle_type == OracleType::OrcaCLMM {
|
||||
let whirlpool = load_whirlpool_state(acc_info)?;
|
||||
let valid_oracle = match oracle_type {
|
||||
OracleType::OrcaCLMM => {
|
||||
let whirlpool = load_orca_pool_state(acc_info)?;
|
||||
whirlpool.has_quote_token()
|
||||
}
|
||||
OracleType::RaydiumCLMM => {
|
||||
let pool = load_raydium_pool_state(acc_info)?;
|
||||
pool.has_quote_token()
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
|
||||
let has_usdc_token = whirlpool.token_mint_a == usdc_mint_mainnet::ID
|
||||
|| whirlpool.token_mint_b == usdc_mint_mainnet::ID;
|
||||
let has_sol_token = whirlpool.token_mint_a == sol_mint_mainnet::ID
|
||||
|| whirlpool.token_mint_b == sol_mint_mainnet::ID;
|
||||
require!(
|
||||
has_usdc_token || has_sol_token,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
}
|
||||
require!(valid_oracle, MangoError::UnexpectedOracle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -253,7 +257,7 @@ fn pyth_get_price(
|
|||
}
|
||||
}
|
||||
|
||||
fn get_pyth_state(
|
||||
pub fn get_pyth_state(
|
||||
acc_info: &(impl KeyedAccountReader + ?Sized),
|
||||
base_decimals: u8,
|
||||
) -> Result<OracleState> {
|
||||
|
@ -404,56 +408,32 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
|
|||
}
|
||||
}
|
||||
OracleType::OrcaCLMM => {
|
||||
let whirlpool = load_whirlpool_state(oracle_info)?;
|
||||
|
||||
let inverted = whirlpool.is_inverted();
|
||||
let quote_state = if inverted {
|
||||
quote_state_unchecked(acc_infos, &whirlpool.token_mint_a)?
|
||||
} else {
|
||||
quote_state_unchecked(acc_infos, &whirlpool.token_mint_b)?
|
||||
};
|
||||
|
||||
let clmm_price = if inverted {
|
||||
let sqrt_price = U64F64::from_bits(whirlpool.sqrt_price).to_num::<f64>();
|
||||
let inverted_price = sqrt_price * sqrt_price;
|
||||
I80F48::from_num(1.0f64 / inverted_price)
|
||||
} else {
|
||||
let sqrt_price = U64F64::from_bits(whirlpool.sqrt_price);
|
||||
I80F48::from_num(sqrt_price * sqrt_price)
|
||||
};
|
||||
|
||||
let price = clmm_price * quote_state.price;
|
||||
let whirlpool = load_orca_pool_state(oracle_info)?;
|
||||
let clmm_price = whirlpool.get_clmm_price();
|
||||
let quote_oracle_state = whirlpool.quote_state_unchecked(acc_infos)?;
|
||||
let price = clmm_price * quote_oracle_state.price;
|
||||
OracleState {
|
||||
price,
|
||||
last_update_slot: quote_state.last_update_slot,
|
||||
deviation: quote_state.deviation,
|
||||
last_update_slot: quote_oracle_state.last_update_slot,
|
||||
deviation: quote_oracle_state.deviation,
|
||||
oracle_type: OracleType::OrcaCLMM,
|
||||
}
|
||||
}
|
||||
OracleType::RaydiumCLMM => {
|
||||
let whirlpool = load_raydium_pool_state(oracle_info)?;
|
||||
let clmm_price = whirlpool.get_clmm_price();
|
||||
let quote_oracle_state = whirlpool.quote_state_unchecked(acc_infos)?;
|
||||
let price = clmm_price * quote_oracle_state.price;
|
||||
OracleState {
|
||||
price,
|
||||
last_update_slot: quote_oracle_state.last_update_slot,
|
||||
deviation: quote_oracle_state.deviation,
|
||||
oracle_type: OracleType::RaydiumCLMM,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn quote_state_unchecked<T: KeyedAccountReader>(
|
||||
acc_infos: &OracleAccountInfos<T>,
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<OracleState> {
|
||||
if quote_mint == &usdc_mint_mainnet::ID {
|
||||
let usd_feed = acc_infos
|
||||
.usdc_opt
|
||||
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
|
||||
let usd_state = get_pyth_state(usd_feed, QUOTE_DECIMALS as u8)?;
|
||||
return Ok(usd_state);
|
||||
} else if quote_mint == &sol_mint_mainnet::ID {
|
||||
let sol_feed = acc_infos
|
||||
.sol_opt
|
||||
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
|
||||
let sol_state = get_pyth_state(sol_feed, SOL_DECIMALS as u8)?;
|
||||
return Ok(sol_state);
|
||||
} else {
|
||||
return Err(MangoError::MissingFeedForCLMMOracle.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn oracle_log_context(
|
||||
name: &str,
|
||||
state: &OracleState,
|
||||
|
@ -545,7 +525,87 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_clmm_price() -> Result<()> {
|
||||
pub fn test_clmm_prices() -> Result<()> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
||||
let usdc_fixture = (
|
||||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
||||
OracleType::Pyth,
|
||||
Pubkey::default(),
|
||||
6,
|
||||
);
|
||||
|
||||
let clmm_fixtures = vec![
|
||||
(
|
||||
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
|
||||
OracleType::OrcaCLMM,
|
||||
orca_mainnet_whirlpool::ID,
|
||||
9, // SOL/USDC pool
|
||||
),
|
||||
(
|
||||
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
|
||||
OracleType::RaydiumCLMM,
|
||||
raydium_mainnet::ID,
|
||||
9, // SOL/USDC pool
|
||||
),
|
||||
];
|
||||
|
||||
for fixture in clmm_fixtures {
|
||||
let clmm_file = format!("resources/test/{}.bin", fixture.0);
|
||||
let mut clmm_data = read_file(find_file(&clmm_file).unwrap());
|
||||
let data = RefCell::new(&mut clmm_data[..]);
|
||||
let ai = &AccountInfoRef {
|
||||
key: &Pubkey::from_str(fixture.0).unwrap(),
|
||||
owner: &fixture.2,
|
||||
data: data.borrow(),
|
||||
};
|
||||
|
||||
let pyth_file = format!("resources/test/{}.bin", usdc_fixture.0);
|
||||
let mut pyth_data = read_file(find_file(&pyth_file).unwrap());
|
||||
let pyth_data_cell = RefCell::new(&mut pyth_data[..]);
|
||||
let usdc_ai = &AccountInfoRef {
|
||||
key: &Pubkey::from_str(usdc_fixture.0).unwrap(),
|
||||
owner: &usdc_fixture.2,
|
||||
data: pyth_data_cell.borrow(),
|
||||
};
|
||||
let base_decimals = fixture.3;
|
||||
let usdc_decimals = usdc_fixture.3;
|
||||
|
||||
let usdc_ais = OracleAccountInfos {
|
||||
oracle: usdc_ai,
|
||||
fallback_opt: None,
|
||||
usdc_opt: None,
|
||||
sol_opt: None,
|
||||
};
|
||||
let clmm_ais = OracleAccountInfos {
|
||||
oracle: ai,
|
||||
fallback_opt: None,
|
||||
usdc_opt: Some(usdc_ai),
|
||||
sol_opt: None,
|
||||
};
|
||||
let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap();
|
||||
let clmm = oracle_state_unchecked(&clmm_ais, base_decimals).unwrap();
|
||||
assert!(usdc.price == I80F48::from_num(1.00000758274099));
|
||||
|
||||
match fixture.1 {
|
||||
OracleType::OrcaCLMM => {
|
||||
// 63.006792786538313 * 1.00000758274099 (but in native/native)
|
||||
assert!(clmm.price == I80F48::from_num(0.06300727055072872))
|
||||
}
|
||||
OracleType::RaydiumCLMM => {
|
||||
// 83.551469620431 * 1.00000758274099 (but in native/native)
|
||||
assert!(clmm.price == I80F48::from_num(0.083552103169584))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_clmm_price_missing_usdc() -> Result<()> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
@ -558,67 +618,13 @@ mod tests {
|
|||
9, // SOL/USDC pool
|
||||
),
|
||||
(
|
||||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
||||
OracleType::Pyth,
|
||||
Pubkey::default(),
|
||||
6,
|
||||
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
|
||||
OracleType::RaydiumCLMM,
|
||||
raydium_mainnet::ID,
|
||||
9, // SOL/USDC pool
|
||||
),
|
||||
];
|
||||
|
||||
let clmm_file = format!("resources/test/{}.bin", fixtures[0].0);
|
||||
let mut clmm_data = read_file(find_file(&clmm_file).unwrap());
|
||||
let data = RefCell::new(&mut clmm_data[..]);
|
||||
let ai = &AccountInfoRef {
|
||||
key: &Pubkey::from_str(fixtures[0].0).unwrap(),
|
||||
owner: &fixtures[0].2,
|
||||
data: data.borrow(),
|
||||
};
|
||||
|
||||
let pyth_file = format!("resources/test/{}.bin", fixtures[1].0);
|
||||
let mut pyth_data = read_file(find_file(&pyth_file).unwrap());
|
||||
let pyth_data_cell = RefCell::new(&mut pyth_data[..]);
|
||||
let usdc_ai = &AccountInfoRef {
|
||||
key: &Pubkey::from_str(fixtures[1].0).unwrap(),
|
||||
owner: &fixtures[1].2,
|
||||
data: pyth_data_cell.borrow(),
|
||||
};
|
||||
let base_decimals = fixtures[0].3;
|
||||
let usdc_decimals = fixtures[1].3;
|
||||
|
||||
let usdc_ais = OracleAccountInfos {
|
||||
oracle: usdc_ai,
|
||||
fallback_opt: None,
|
||||
usdc_opt: None,
|
||||
sol_opt: None,
|
||||
};
|
||||
let orca_ais = OracleAccountInfos {
|
||||
oracle: ai,
|
||||
fallback_opt: None,
|
||||
usdc_opt: Some(usdc_ai),
|
||||
sol_opt: None,
|
||||
};
|
||||
let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap();
|
||||
let orca = oracle_state_unchecked(&orca_ais, base_decimals).unwrap();
|
||||
assert!(usdc.price == I80F48::from_num(1.00000758274099));
|
||||
// 63.006792786538313 * 1.00000758274099 (but in native/native)
|
||||
assert!(orca.price == I80F48::from_num(0.06300727055072872));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_clmm_price_missing_usdc() -> Result<()> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
||||
let fixtures = vec![(
|
||||
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
|
||||
OracleType::OrcaCLMM,
|
||||
orca_mainnet_whirlpool::ID,
|
||||
9, // SOL/USDC pool
|
||||
)];
|
||||
|
||||
for fixture in fixtures {
|
||||
let filename = format!("resources/test/{}.bin", fixture.0);
|
||||
let mut clmm_data = read_file(find_file(&filename).unwrap());
|
||||
|
@ -642,4 +648,47 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_valid_fallbacks() -> Result<()> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
||||
let usdc_fixture = (
|
||||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
||||
OracleType::Pyth,
|
||||
Pubkey::default(),
|
||||
6,
|
||||
);
|
||||
|
||||
let clmm_fixtures = vec![
|
||||
(
|
||||
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
|
||||
OracleType::OrcaCLMM,
|
||||
orca_mainnet_whirlpool::ID,
|
||||
9, // SOL/USDC pool
|
||||
),
|
||||
(
|
||||
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
|
||||
OracleType::RaydiumCLMM,
|
||||
raydium_mainnet::ID,
|
||||
9, // SOL/USDC pool
|
||||
),
|
||||
];
|
||||
|
||||
for fixture in clmm_fixtures {
|
||||
let clmm_file = format!("resources/test/{}.bin", fixture.0);
|
||||
let mut clmm_data = read_file(find_file(&clmm_file).unwrap());
|
||||
let data = RefCell::new(&mut clmm_data[..]);
|
||||
let ai = &AccountInfoRef {
|
||||
key: &Pubkey::from_str(fixture.0).unwrap(),
|
||||
owner: &fixture.2,
|
||||
data: data.borrow(),
|
||||
};
|
||||
|
||||
check_is_valid_fallback_oracle(ai)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use crate::{accounts_zerocopy::KeyedAccountReader, error::MangoError};
|
||||
|
||||
use super::{
|
||||
pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, sol_mint_mainnet, usdc_mint_mainnet,
|
||||
};
|
||||
|
||||
pub mod orca_mainnet_whirlpool {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");
|
||||
}
|
||||
|
||||
pub const ORCA_WHIRLPOOL_LEN: usize = 653;
|
||||
pub const ORCA_WHIRLPOOL_DISCRIMINATOR: [u8; 8] = [63, 149, 209, 12, 225, 128, 99, 9];
|
||||
|
||||
pub struct WhirlpoolState {
|
||||
// Q64.64
|
||||
pub sqrt_price: u128, // 16
|
||||
pub token_mint_a: Pubkey, // 32
|
||||
pub token_mint_b: Pubkey, // 32
|
||||
}
|
||||
|
||||
impl WhirlpoolState {
|
||||
pub fn is_inverted(&self) -> bool {
|
||||
self.token_mint_a == usdc_mint_mainnet::ID
|
||||
|| (self.token_mint_a == sol_mint_mainnet::ID
|
||||
&& self.token_mint_b != usdc_mint_mainnet::ID)
|
||||
}
|
||||
|
||||
pub fn get_quote_oracle(&self) -> Result<Pubkey> {
|
||||
let mint = if self.is_inverted() {
|
||||
self.token_mint_a
|
||||
} else {
|
||||
self.token_mint_b
|
||||
};
|
||||
|
||||
if mint == usdc_mint_mainnet::ID {
|
||||
return Ok(pyth_mainnet_usdc_oracle::ID);
|
||||
} else if mint == sol_mint_mainnet::ID {
|
||||
return Ok(pyth_mainnet_sol_oracle::ID);
|
||||
} else {
|
||||
return Err(MangoError::MissingFeedForCLMMOracle.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_whirlpool_state(acc_info: &impl KeyedAccountReader) -> Result<WhirlpoolState> {
|
||||
let data = &acc_info.data();
|
||||
require!(
|
||||
data[0..8] == ORCA_WHIRLPOOL_DISCRIMINATOR[..],
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
require!(
|
||||
data.len() == ORCA_WHIRLPOOL_LEN,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
require!(
|
||||
acc_info.owner() == &orca_mainnet_whirlpool::ID,
|
||||
MangoError::InvalidCLMMOracle
|
||||
);
|
||||
|
||||
let price_bytes: &[u8; 16] = &data[65..81].try_into().unwrap();
|
||||
let sqrt_price = u128::from_le_bytes(*price_bytes);
|
||||
let a: &[u8; 32] = &(&data[101..133]).try_into().unwrap();
|
||||
let b: &[u8; 32] = &(&data[181..213]).try_into().unwrap();
|
||||
let mint_a = Pubkey::from(*a);
|
||||
let mint_b = Pubkey::from(*b);
|
||||
|
||||
Ok(WhirlpoolState {
|
||||
sqrt_price,
|
||||
token_mint_a: mint_a,
|
||||
token_mint_b: mint_b,
|
||||
})
|
||||
}
|
|
@ -31,6 +31,12 @@ pub fn format_zero_terminated_utf8_bytes(
|
|||
)
|
||||
}
|
||||
|
||||
// Returns (now_ts, now_slot)
|
||||
pub fn clock_now() -> (u64, u64) {
|
||||
let clock = Clock::get().unwrap();
|
||||
(clock.unix_timestamp.try_into().unwrap(), clock.slot)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -10,7 +10,8 @@ pub use program_test::*;
|
|||
|
||||
pub use super::program_test;
|
||||
|
||||
pub use utils::assert_equal_fixed_f64 as assert_equal;
|
||||
pub use crate::assert_eq_f64;
|
||||
pub use crate::assert_eq_fixed_f64;
|
||||
|
||||
mod test_alt;
|
||||
mod test_bankrupt_tokens;
|
||||
|
@ -21,6 +22,7 @@ mod test_collateral_fees;
|
|||
mod test_delegate;
|
||||
mod test_fees_buyback_with_mngo;
|
||||
mod test_force_close;
|
||||
mod test_health_check;
|
||||
mod test_health_compute;
|
||||
mod test_health_region;
|
||||
mod test_ix_gate_set;
|
||||
|
@ -35,6 +37,7 @@ mod test_perp_settle;
|
|||
mod test_perp_settle_fees;
|
||||
mod test_position_lifetime;
|
||||
mod test_reduce_only;
|
||||
mod test_replay;
|
||||
mod test_serum;
|
||||
mod test_stale_oracles;
|
||||
mod test_token_conditional_swap;
|
||||
|
|
|
@ -450,7 +450,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
|||
.await;
|
||||
|
||||
let maint_health = account_maint_health(solana, account).await;
|
||||
assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2));
|
||||
assert_eq_f64!(maint_health, 1000.0, 1e-2);
|
||||
|
||||
let start_time = solana.clock_timestamp().await;
|
||||
|
||||
|
@ -476,17 +476,17 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
|
||||
let maint_health = account_maint_health(solana, account).await;
|
||||
assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2));
|
||||
assert_eq_f64!(maint_health, 1000.0, 1e-2);
|
||||
|
||||
solana.set_clock_timestamp(start_time + 1500).await;
|
||||
|
||||
let maint_health = account_maint_health(solana, account).await;
|
||||
assert!(assert_equal_f64_f64(maint_health, 750.0, 1e-2));
|
||||
assert_eq_f64!(maint_health, 750.0, 1e-2);
|
||||
|
||||
solana.set_clock_timestamp(start_time + 3000).await;
|
||||
|
||||
let maint_health = account_maint_health(solana, account).await;
|
||||
assert!(assert_equal_f64_f64(maint_health, 500.0, 1e-2));
|
||||
assert_eq_f64!(maint_health, 500.0, 1e-2);
|
||||
|
||||
solana.set_clock_timestamp(start_time + 1600).await;
|
||||
|
||||
|
@ -507,11 +507,11 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
|
||||
let maint_health = account_maint_health(solana, account).await;
|
||||
assert!(assert_equal_f64_f64(maint_health, 700.0, 1e-2));
|
||||
assert_eq_f64!(maint_health, 700.0, 1e-2);
|
||||
|
||||
let bank: Bank = solana.get_account(tokens[0].bank).await;
|
||||
assert!(assert_equal_fixed_f64(bank.maint_asset_weight, 0.7, 1e-4));
|
||||
assert!(assert_equal_fixed_f64(bank.maint_liab_weight, 1.3, 1e-4));
|
||||
assert_eq_fixed_f64!(bank.maint_asset_weight, 0.7, 1e-4);
|
||||
assert_eq_fixed_f64!(bank.maint_liab_weight, 1.3, 1e-4);
|
||||
assert_eq!(bank.maint_weight_shift_duration_inv, I80F48::ZERO);
|
||||
|
||||
Ok(())
|
||||
|
@ -687,3 +687,365 @@ async fn test_bank_deposit_limit() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_withdraw_skip_bank() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let payer_token_accounts = &context.users[1].token_accounts;
|
||||
let mints = &context.mints[0..3];
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
zero_token_is_quote: true,
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// Funding to fill the vaults
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints,
|
||||
1_000_000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[0..2],
|
||||
1000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: when all balances are positive
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// ok even when total health = 0
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank, tokens[1].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1001,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank],
|
||||
},
|
||||
MangoError::BorrowsRequireHealthAccountBank
|
||||
);
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1001,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
MangoError::HealthMustBePositiveOrIncrease
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: create a borrow
|
||||
//
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[2],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank, tokens[1].bank],
|
||||
},
|
||||
MangoError::HealthMustBePositiveOrIncrease
|
||||
);
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[2],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[2].bank],
|
||||
},
|
||||
MangoError::BorrowsRequireHealthAccountBank
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[2],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: withdraw positive balances when there's a borrow
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[2].bank],
|
||||
},
|
||||
MangoError::InvalidBank
|
||||
);
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank, tokens[1].bank],
|
||||
},
|
||||
MangoError::HealthMustBePositive
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sequence_check() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..1];
|
||||
|
||||
let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
token_count: 6,
|
||||
serum3_count: 3,
|
||||
perp_count: 3,
|
||||
perp_oo_count: 3,
|
||||
token_conditional_swap_count: 3,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
let mango_account = get_mango_account(solana, account).await;
|
||||
assert_eq!(mango_account.fixed.sequence_number, 0);
|
||||
|
||||
//
|
||||
// TEST: Sequence check with right sequence number
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
SequenceCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
expected_sequence_number: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mango_account = get_mango_account(solana, account).await;
|
||||
assert_eq!(mango_account.fixed.sequence_number, 1);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
SequenceCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
expected_sequence_number: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mango_account = get_mango_account(solana, account).await;
|
||||
assert_eq!(mango_account.fixed.sequence_number, 2);
|
||||
|
||||
//
|
||||
// TEST: Sequence check with wrong sequence number
|
||||
//
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
SequenceCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
expected_sequence_number: 1
|
||||
},
|
||||
MangoError::InvalidSequenceNumber
|
||||
);
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
SequenceCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
expected_sequence_number: 4
|
||||
},
|
||||
MangoError::InvalidSequenceNumber
|
||||
);
|
||||
|
||||
let mango_account = get_mango_account(solana, account).await;
|
||||
assert_eq!(mango_account.fixed.sequence_number, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
|||
|
||||
// fails because borrow is greater than remaining margin in net borrow limit
|
||||
// (requires the test to be quick enough to avoid accidentally going to the next borrow limit window!)
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 4000,
|
||||
|
@ -231,12 +231,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
|||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::BankNetBorrowsLimitReached.into(),
|
||||
"".into(),
|
||||
MangoError::BankNetBorrowsLimitReached
|
||||
);
|
||||
|
||||
// succeeds because is not a borrow
|
||||
|
@ -314,7 +309,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
|||
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 10.0).await;
|
||||
|
||||
// cannot borrow anything: net borrowed 1002 * price 10.0 > limit 6000
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
|
@ -324,12 +319,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
|||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::BankNetBorrowsLimitReached.into(),
|
||||
"".into(),
|
||||
MangoError::BankNetBorrowsLimitReached
|
||||
);
|
||||
|
||||
// can still withdraw
|
||||
|
@ -350,7 +340,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
|||
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 5.0).await;
|
||||
|
||||
// cannot borrow this much: (net borrowed 1000 + new borrow 201) * price 5.0 > limit 6000
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 200,
|
||||
|
@ -360,12 +350,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
|||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::BankNetBorrowsLimitReached.into(),
|
||||
"".into(),
|
||||
MangoError::BankNetBorrowsLimitReached
|
||||
);
|
||||
|
||||
// can borrow smaller amounts: (net borrowed 1000 + new borrow 199) * price 5.0 < limit 6000
|
||||
|
|
|
@ -177,11 +177,11 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
|
|||
.await
|
||||
.unwrap();
|
||||
last_time = solana.clock_timestamp().await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
assert_eq_f64!(
|
||||
account_position_f64(solana, account, tokens[0].bank).await,
|
||||
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
|
||||
0.01
|
||||
));
|
||||
);
|
||||
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
|
||||
|
||||
//
|
||||
|
@ -208,11 +208,11 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
|
|||
.await
|
||||
.unwrap();
|
||||
//last_time = solana.clock_timestamp().await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
assert_eq_f64!(
|
||||
account_position_f64(solana, account, tokens[0].bank).await,
|
||||
last_balance * (1.0 - 0.1 * (7.0 / 24.0) * (720.0 / (last_balance * 0.8))),
|
||||
0.01
|
||||
));
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -185,16 +185,16 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> {
|
|||
assert_eq!(before_fees_accrued - after_fees_accrued, 19);
|
||||
|
||||
// token[1] swapped at discount for token[0]
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
fees_token_position_after - fees_token_position_before,
|
||||
19.0 / 2.0,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
mngo_token_position_after - mngo_token_position_before,
|
||||
-19.0 / 3.0 / 1.2,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -357,18 +357,18 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
|
|||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
-99.99,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
99.98,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
// Market needs to be in force close
|
||||
assert!(send_tx(
|
||||
|
@ -423,18 +423,18 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
|
|||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
0.009,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
-0.0199,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
use crate::cases::{
|
||||
create_funded_account, mango_setup, send_tx, tokio, HealthAccountSkipping,
|
||||
HealthCheckInstruction, TestContext, TestKeypair, TokenWithdrawInstruction,
|
||||
};
|
||||
use crate::send_tx_expect_error;
|
||||
use mango_v4::accounts_ix::{HealthCheck, HealthCheckKind};
|
||||
use mango_v4::error::MangoError;
|
||||
use solana_sdk::transport::TransportError;
|
||||
|
||||
// TODO FAS
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_check() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let payer_token_accounts = &context.users[1].token_accounts;
|
||||
let mints = &context.mints[0..3];
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
zero_token_is_quote: true,
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// Funding to fill the vaults
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints,
|
||||
1_000_000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[0..2],
|
||||
1000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 775,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[2],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST (Health is about 93% with all banks, 7% without banks 1)
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
min_health_value: 20.0,
|
||||
check_kind: HealthCheckKind::MaintRatio,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
min_health_value: 500.0,
|
||||
check_kind: HealthCheckKind::Init,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthCheckInstruction {
|
||||
owner,
|
||||
account,
|
||||
min_health_value: 600.0,
|
||||
check_kind: HealthCheckKind::Init,
|
||||
},
|
||||
MangoError::InvalidHealth
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthCheckInstruction {
|
||||
account,
|
||||
owner,
|
||||
min_health_value: 800.0,
|
||||
check_kind: HealthCheckKind::Maint,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthCheckInstruction {
|
||||
owner,
|
||||
account,
|
||||
min_health_value: 100.0,
|
||||
check_kind: HealthCheckKind::MaintRatio,
|
||||
},
|
||||
MangoError::InvalidHealth
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: HealthCheckInstruction {
|
||||
owner,
|
||||
account,
|
||||
min_health_value: 5.0,
|
||||
check_kind: HealthCheckKind::MaintRatio,
|
||||
},
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: HealthCheckInstruction {
|
||||
owner,
|
||||
account,
|
||||
min_health_value: 10.0,
|
||||
check_kind: HealthCheckKind::MaintRatio,
|
||||
},
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
MangoError::InvalidHealth
|
||||
);
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: HealthCheckInstruction {
|
||||
owner,
|
||||
account,
|
||||
min_health_value: 10.0,
|
||||
check_kind: HealthCheckKind::MaintRatio,
|
||||
},
|
||||
skip_banks: vec![tokens[2].bank],
|
||||
},
|
||||
MangoError::InvalidBank
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -344,7 +344,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
|
|||
#[tokio::test]
|
||||
async fn test_health_compute_serum() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(135_000);
|
||||
test_builder.test().set_compute_max_units(137_000);
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
|
|
@ -337,11 +337,11 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
|
|||
liqor_quote_before + 12
|
||||
);
|
||||
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
acc_data.perps[0].quote_position_native(),
|
||||
-50.0 + 11.0 + 27.0,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
assert_eq!(acc_data.being_liquidated, 0);
|
||||
let (_liqor_data, liqor_perp) = liqor_info(perp_market, liqor).await;
|
||||
assert_eq!(liqor_perp.quote_position_native(), -11);
|
||||
|
|
|
@ -217,35 +217,35 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
let liqee_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.05);
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
-liqor_amount,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
-20.0 * 100.0 + liqee_amount,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].realized_pnl_for_position_native,
|
||||
liqee_amount - 1000.0,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
// stable price is 1.0, so 0.2 * 1000
|
||||
assert_eq!(liqee_data.perps[0].recurring_settle_pnl_allowance, 201);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
|
||||
liqor_amount - liqee_amount,
|
||||
0.1,
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
|
||||
liqor_amount - liqee_amount,
|
||||
0.1,
|
||||
));
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Liquidate base position max
|
||||
|
@ -268,18 +268,18 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
let liqee_amount_2 = 6.0 * 100.0 * 0.6 * (1.0 - 0.05);
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 6);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
-liqor_amount - liqor_amount_2,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
-20.0 * 100.0 + liqee_amount + liqee_amount_2,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
// verify health is good again
|
||||
send_tx(
|
||||
|
@ -339,28 +339,28 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
let liqee_amount_3 = 10.0 * 100.0 * 1.32 * (1.0 + 0.05);
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 16 - 10);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
-liqor_amount - liqor_amount_2 + liqor_amount_3,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), -10);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
20.0 * 100.0 - liqee_amount_3,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
|
||||
liqee_amount_3 - liqor_amount_3,
|
||||
0.1,
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
|
||||
liqee_amount_3 - liqor_amount_3,
|
||||
0.1,
|
||||
));
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Liquidate base position max
|
||||
|
@ -383,18 +383,18 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
let liqee_amount_4 = 7.0 * 100.0 * 1.32 * (1.0 + 0.05);
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 6 - 7);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), -3);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
// verify health is good again
|
||||
send_tx(
|
||||
|
@ -438,18 +438,18 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
let liqee_amount_5 = 3.0 * 100.0 * 2.0 * (1.0 + 0.05);
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 3);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4 + liqor_amount_5,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4 - liqee_amount_5,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Can settle-pnl even though health is negative
|
||||
|
@ -481,11 +481,11 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
assert!(remaining_pnl < 0.0);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
remaining_pnl,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account_1, quote_token.bank).await,
|
||||
liqee_quote_deposits_before as i64
|
||||
|
@ -566,27 +566,27 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
|
||||
// insurance fund was depleted and the liqor received it
|
||||
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.tokens[0].native("e_bank),
|
||||
liqor_before.tokens[0].native("e_bank).to_num::<f64>() + insurance_vault_funding as f64,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.tokens[1].native(&settle_bank),
|
||||
liqor_before.tokens[1].native(&settle_bank).to_num::<f64>()
|
||||
- liqee_settle_limit_before as f64,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
// liqor took over the max possible negative pnl
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
liqor_before.perps[0]
|
||||
.quote_position_native()
|
||||
.to_num::<f64>()
|
||||
- liq_perp_quote_amount,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
// liqee exited liquidation
|
||||
assert!(account_init_health(solana, account_1).await >= 0.0);
|
||||
|
@ -602,21 +602,21 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
|
||||
let open_interest = 2 * liqor_data.perps[0].base_position_lots.abs();
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
perp_market.long_funding,
|
||||
socialized_amount / open_interest as f64,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
perp_market.short_funding,
|
||||
-socialized_amount / open_interest as f64,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
account0_before.perps[0].unsettled_funding(&perp_market),
|
||||
socialized_amount / 2.0,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -297,22 +297,22 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
|
|||
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
100.0 + 600.0 - 2100.0 * 0.95,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, liqor, settle_token.bank).await,
|
||||
10000 - 95 - 570
|
||||
);
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 9);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
-10000.0 - 100.0 - 600.0 + 2100.0 * 0.95,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account_0, settle_token.bank).await,
|
||||
95 + 570
|
||||
|
|
|
@ -421,24 +421,16 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
// The liqee pays for the 20 collateral at a price of 1.02*1.02. The liqor gets 1.01*1.01,
|
||||
// so the platform fee is
|
||||
let platform_fee = 20.0 * (1.0 - 1.01 * 1.01 / (1.02 * 1.02));
|
||||
assert!(assert_equal_f64_f64(
|
||||
assert_eq_f64!(
|
||||
account_position_f64(solana, vault_account, collateral_token2.bank).await,
|
||||
100000.0 + 20.0 - platform_fee,
|
||||
0.001,
|
||||
));
|
||||
);
|
||||
|
||||
// Verify platform liq fee tracking
|
||||
let colbank = solana.get_account::<Bank>(collateral_token2.bank).await;
|
||||
assert!(assert_equal_fixed_f64(
|
||||
colbank.collected_fees_native,
|
||||
platform_fee,
|
||||
0.001
|
||||
));
|
||||
assert!(assert_equal_fixed_f64(
|
||||
colbank.collected_liquidation_fees,
|
||||
platform_fee,
|
||||
0.001
|
||||
));
|
||||
assert_eq_fixed_f64!(colbank.collected_fees_native, platform_fee, 0.001);
|
||||
assert_eq_fixed_f64!(colbank.collected_liquidation_fees, platform_fee, 0.001);
|
||||
|
||||
let liqee = get_mango_account(solana, account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
|
|
|
@ -16,9 +16,6 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
let payer_mint0_account = context.users[1].token_accounts[0];
|
||||
let loan_origination_fee = 0.0005;
|
||||
|
||||
// higher resolution that the loan_origination_fee for one token
|
||||
let balance_f64eq = |a: f64, b: f64| utils::assert_equal_f64_f64(a, b, 0.0001);
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
@ -173,10 +170,11 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
margin_account_initial + withdraw_amount - deposit_amount
|
||||
);
|
||||
// no fee because user had positive balance
|
||||
assert!(balance_f64eq(
|
||||
assert_eq_f64!(
|
||||
account_position_f64(solana, account, bank).await,
|
||||
(deposit_amount_initial - withdraw_amount + deposit_amount) as f64
|
||||
));
|
||||
(deposit_amount_initial - withdraw_amount + deposit_amount) as f64,
|
||||
0.0001
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Bringing the balance to 0 deactivates the token
|
||||
|
@ -210,10 +208,11 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
solana.token_account_balance(margin_account).await,
|
||||
margin_account_initial - deposit_amount
|
||||
);
|
||||
assert!(balance_f64eq(
|
||||
assert_eq_f64!(
|
||||
account_position_f64(solana, account, bank).await,
|
||||
deposit_amount as f64
|
||||
));
|
||||
deposit_amount as f64,
|
||||
0.0001
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Try loan fees by withdrawing more than the user balance
|
||||
|
@ -232,11 +231,12 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
|
|||
solana.token_account_balance(margin_account).await,
|
||||
margin_account_initial + withdraw_amount - deposit_amount
|
||||
);
|
||||
assert!(balance_f64eq(
|
||||
assert_eq_f64!(
|
||||
account_position_f64(solana, account, bank).await,
|
||||
(deposit_amount_initial + deposit_amount - withdraw_amount) as f64
|
||||
- (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee
|
||||
));
|
||||
- (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee,
|
||||
0.0001
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -255,9 +255,6 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
|
|||
let owner_accounts = context.users[0].token_accounts.clone();
|
||||
let payer_accounts = context.users[1].token_accounts.clone();
|
||||
|
||||
// higher resolution that the loan_origination_fee for one token
|
||||
let balance_f64eq = |a: f64, b: f64| utils::assert_equal_f64_f64(a, b, 0.0001);
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
@ -414,13 +411,14 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
|
|||
);
|
||||
|
||||
let mango_withdraw_amount = account_position_f64(solana, account, tokens[0].bank).await;
|
||||
assert!(balance_f64eq(
|
||||
assert_eq_f64!(
|
||||
mango_withdraw_amount,
|
||||
initial_deposit as f64 - withdraw_amount as f64 * (1.0 + swap_fee_rate)
|
||||
));
|
||||
initial_deposit as f64 - withdraw_amount as f64 * (1.0 + swap_fee_rate),
|
||||
0.0001
|
||||
);
|
||||
|
||||
let mango_deposit_amount = account_position_f64(solana, account, tokens[1].bank).await;
|
||||
assert!(balance_f64eq(mango_deposit_amount, deposit_amount as f64));
|
||||
assert_eq_f64!(mango_deposit_amount, deposit_amount as f64, 0.0001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -732,3 +730,112 @@ async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_margin_trade_skip_bank() -> Result<(), BanksClientError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(100_000);
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
let payer_mint0_account = context.users[1].token_accounts[0];
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let bank = tokens[0].bank;
|
||||
|
||||
//
|
||||
// create the test user account
|
||||
//
|
||||
|
||||
let deposit_amount_initial = 100;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints,
|
||||
deposit_amount_initial,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: Margin trade
|
||||
//
|
||||
let margin_account = payer_mint0_account;
|
||||
let target_token_account = context.users[0].token_accounts[0];
|
||||
let make_flash_loan_tx = |solana, deposit_amount, skip_banks| async move {
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
let loans = vec![FlashLoanPart {
|
||||
bank,
|
||||
token_account: target_token_account,
|
||||
withdraw_amount: 0,
|
||||
}];
|
||||
tx.add_instruction(FlashLoanBeginInstruction {
|
||||
account,
|
||||
owner,
|
||||
loans: loans.clone(),
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction_direct(
|
||||
spl_token::instruction::transfer(
|
||||
&spl_token::ID,
|
||||
&margin_account,
|
||||
&target_token_account,
|
||||
&payer.pubkey(),
|
||||
&[&payer.pubkey()],
|
||||
deposit_amount,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
tx.add_signer(payer);
|
||||
tx.add_instruction(HealthAccountSkipping {
|
||||
inner: FlashLoanEndInstruction {
|
||||
account,
|
||||
owner,
|
||||
loans,
|
||||
// the test only accesses a single token: not a swap
|
||||
flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown,
|
||||
},
|
||||
skip_banks,
|
||||
})
|
||||
.await;
|
||||
tx
|
||||
};
|
||||
|
||||
make_flash_loan_tx(solana, 1, vec![])
|
||||
.await
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
make_flash_loan_tx(solana, 1, vec![tokens[1].bank])
|
||||
.await
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
make_flash_loan_tx(solana, 1, vec![tokens[0].bank])
|
||||
.await
|
||||
.send_expect_error(MangoError::InvalidBank)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -287,19 +287,19 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
|
|||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
-99.99,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
99.98,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: closing perp positions
|
||||
|
@ -364,19 +364,19 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
|
|||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
0.02,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
-0.04,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
// settle pnl and fees to bring quote_position_native fully to 0
|
||||
send_tx(
|
||||
|
@ -644,19 +644,19 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
|
|||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 2);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
-19998.0,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -2);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
19996.0,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Place a pegged order and check how it behaves with oracle changes
|
||||
|
@ -1008,30 +1008,18 @@ async fn test_perp_realize_partially() -> Result<(), TransportError> {
|
|||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
let perp_0 = mango_account_0.perps[0];
|
||||
assert_eq!(perp_0.base_position_lots(), 1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
perp_0.quote_position_native(),
|
||||
-200_000.0 + 150_000.0,
|
||||
0.001
|
||||
));
|
||||
assert!(assert_equal(
|
||||
perp_0.realized_pnl_for_position_native,
|
||||
50_000.0,
|
||||
0.001
|
||||
));
|
||||
);
|
||||
assert_eq_fixed_f64!(perp_0.realized_pnl_for_position_native, 50_000.0, 0.001);
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
let perp_1 = mango_account_1.perps[0];
|
||||
assert_eq!(perp_1.base_position_lots(), -1);
|
||||
assert!(assert_equal(
|
||||
perp_1.quote_position_native(),
|
||||
200_000.0 - 150_000.0,
|
||||
0.001
|
||||
));
|
||||
assert!(assert_equal(
|
||||
perp_1.realized_pnl_for_position_native,
|
||||
-50_000.0,
|
||||
0.001
|
||||
));
|
||||
assert_eq_fixed_f64!(perp_1.quote_position_native(), 200_000.0 - 150_000.0, 0.001);
|
||||
assert_eq_fixed_f64!(perp_1.realized_pnl_for_position_native, -50_000.0, 0.001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1593,6 +1581,138 @@ async fn test_perp_cancel_with_in_flight_events() -> Result<(), TransportError>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_perp_skip_bank() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
let deposit_amount = 1000;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Create a perp market
|
||||
//
|
||||
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||
solana,
|
||||
PerpCreateMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
payer,
|
||||
perp_market_index: 0,
|
||||
quote_lot_size: 10,
|
||||
base_lot_size: 100,
|
||||
maint_base_asset_weight: 0.975,
|
||||
init_base_asset_weight: 0.95,
|
||||
maint_base_liab_weight: 1.025,
|
||||
init_base_liab_weight: 1.05,
|
||||
base_liquidation_fee: 0.012,
|
||||
maker_fee: 0.0000,
|
||||
taker_fee: 0.0000,
|
||||
settle_pnl_limit_factor: -1.0,
|
||||
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
let price_lots = perp_market_data.native_price_to_lot(I80F48::from(1));
|
||||
|
||||
//
|
||||
// TESTS
|
||||
//
|
||||
|
||||
// good without skips
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: PerpPlaceOrderInstruction {
|
||||
account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
max_base_lots: 2,
|
||||
client_order_id: 5,
|
||||
..PerpPlaceOrderInstruction::default()
|
||||
},
|
||||
skip_banks: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can skip unrelated
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: PerpPlaceOrderInstruction {
|
||||
account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
max_base_lots: 2,
|
||||
client_order_id: 5,
|
||||
..PerpPlaceOrderInstruction::default()
|
||||
},
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can't skip settle token index
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: PerpPlaceOrderInstruction {
|
||||
account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
max_base_lots: 2,
|
||||
client_order_id: 5,
|
||||
..PerpPlaceOrderInstruction::default()
|
||||
},
|
||||
skip_banks: vec![tokens[0].bank],
|
||||
},
|
||||
MangoError::TokenPositionDoesNotExist,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
|||
}
|
||||
|
||||
// Cannot settle with yourself
|
||||
let result = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
PerpSettlePnlInstruction {
|
||||
settler,
|
||||
|
@ -185,17 +185,11 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
|||
account_b: account_0,
|
||||
perp_market,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_mango_error(
|
||||
&result,
|
||||
MangoError::CannotSettleWithSelf.into(),
|
||||
"Cannot settle with yourself".to_string(),
|
||||
MangoError::CannotSettleWithSelf
|
||||
);
|
||||
|
||||
// Cannot settle position that does not exist
|
||||
let result = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
PerpSettlePnlInstruction {
|
||||
settler,
|
||||
|
@ -204,13 +198,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
|||
account_b: account_1,
|
||||
perp_market: perp_market_2,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_mango_error(
|
||||
&result,
|
||||
MangoError::PerpPositionDoesNotExist.into(),
|
||||
"Cannot settle a position that does not exist".to_string(),
|
||||
MangoError::PerpPositionDoesNotExist
|
||||
);
|
||||
|
||||
// TODO: Test funding settlement
|
||||
|
@ -235,7 +223,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
|||
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1200.0).await;
|
||||
|
||||
// Account a must be the profitable one
|
||||
let result = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
PerpSettlePnlInstruction {
|
||||
settler,
|
||||
|
@ -244,13 +232,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
|||
account_b: account_0,
|
||||
perp_market,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_mango_error(
|
||||
&result,
|
||||
MangoError::ProfitabilityMismatch.into(),
|
||||
"Account a must be the profitable one".to_string(),
|
||||
MangoError::ProfitabilityMismatch
|
||||
);
|
||||
|
||||
// Change the oracle to a more reasonable price
|
||||
|
@ -1038,7 +1020,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
|||
// Test 2: Once the settle limit is exhausted, we can't settle more
|
||||
//
|
||||
// we are in the same window, and we settled max. possible in previous attempt
|
||||
let result = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
PerpSettlePnlInstruction {
|
||||
settler,
|
||||
|
@ -1047,12 +1029,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
|||
account_b: account_1,
|
||||
perp_market,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&result,
|
||||
MangoError::ProfitabilityMismatch.into(),
|
||||
"Account A has no settleable positive pnl left".to_string(),
|
||||
MangoError::ProfitabilityMismatch
|
||||
);
|
||||
|
||||
//
|
||||
|
|
|
@ -166,52 +166,40 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
|
||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0].quote_position_native(),
|
||||
-100_020.0,
|
||||
0.01
|
||||
));
|
||||
);
|
||||
|
||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0].quote_position_native(),
|
||||
100_000.0,
|
||||
0.01
|
||||
));
|
||||
);
|
||||
|
||||
// Cannot settle position that does not exist
|
||||
let result = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
PerpSettleFeesInstruction {
|
||||
account: account_1,
|
||||
perp_market: perp_market_2,
|
||||
max_settle_amount: u64::MAX,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_mango_error(
|
||||
&result,
|
||||
MangoError::PerpPositionDoesNotExist.into(),
|
||||
"Cannot settle a position that does not exist".to_string(),
|
||||
MangoError::PerpPositionDoesNotExist
|
||||
);
|
||||
|
||||
// max_settle_amount must be greater than zero
|
||||
let result = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
PerpSettleFeesInstruction {
|
||||
account: account_1,
|
||||
perp_market: perp_market,
|
||||
max_settle_amount: 0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_mango_error(
|
||||
&result,
|
||||
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
|
||||
"max_settle_amount must be greater than zero".to_string(),
|
||||
MangoError::MaxSettleAmountMustBeGreaterThanZero
|
||||
);
|
||||
|
||||
// TODO: Test funding settlement
|
||||
|
@ -247,20 +235,20 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
|||
// No change
|
||||
{
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_0.perps[0]
|
||||
.unsettled_pnl(&perp_market, I80F48::from(1200))
|
||||
.unwrap(),
|
||||
19980.0, // 1*100*(1200-1000) - (20 in fees)
|
||||
0.01
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
mango_account_1.perps[0]
|
||||
.unsettled_pnl(&perp_market, I80F48::from(1200))
|
||||
.unwrap(),
|
||||
-20000.0,
|
||||
0.01
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Difficult to test health due to fees being so small. Need alternative
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
use super::*;
|
||||
|
||||
use solana_address_lookup_table_program::state::AddressLookupTable;
|
||||
use solana_program::program_pack::Pack;
|
||||
use solana_sdk::account::{Account, ReadableAccount};
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::message::v0::LoadedAddresses;
|
||||
use solana_sdk::message::SanitizedMessage;
|
||||
use solana_sdk::message::SanitizedVersionedMessage;
|
||||
use solana_sdk::message::SimpleAddressLoader;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
use anyhow::Context;
|
||||
use mango_v4::accounts_zerocopy::LoadMutZeroCopy;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn read_json_file<P: AsRef<std::path::Path>>(path: P) -> anyhow::Result<serde_json::Value> {
|
||||
let file_contents = std::fs::read_to_string(path)?;
|
||||
let json: serde_json::Value = serde_json::from_str(&file_contents)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
fn account_from_snapshot(snapshot_path: &str, pk: Pubkey) -> anyhow::Result<Account> {
|
||||
let file_path = format!("{}/{}.json", snapshot_path, pk);
|
||||
let json = read_json_file(&file_path).with_context(|| format!("reading {file_path}"))?;
|
||||
let account = json.get("account").unwrap();
|
||||
let data = base64::decode(
|
||||
account.get("data").unwrap().as_array().unwrap()[0]
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let owner = Pubkey::from_str(account.get("owner").unwrap().as_str().unwrap()).unwrap();
|
||||
let mut account = Account::new(u64::MAX, data.len(), &owner);
|
||||
account.data = data;
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn find_tx(block_file: &str, txsig: &str) -> Option<(u64, i64, Vec<u8>)> {
|
||||
let txsig = bs58::decode(txsig).into_vec().unwrap();
|
||||
let block = read_json_file(block_file).unwrap();
|
||||
let slot = block.get("parentSlot").unwrap().as_u64().unwrap();
|
||||
let time = block.get("blockTime").unwrap().as_i64().unwrap();
|
||||
let txs = block.get("transactions").unwrap().as_array().unwrap();
|
||||
for tx_obj in txs {
|
||||
let tx_bytes = base64::decode(
|
||||
tx_obj.get("transaction").unwrap().as_array().unwrap()[0]
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let sig = &tx_bytes[1..65];
|
||||
if sig == txsig {
|
||||
return Some((slot, time, tx_bytes.to_vec()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_replay() -> anyhow::Result<()> {
|
||||
// Path to a directory generated with cli save-snapshot, containing <pubkey>.json files
|
||||
let snapshot_path = &"path/to/directory";
|
||||
// Path to the block data, retrieved with `solana block 252979760 --output json`
|
||||
let block_file = &"path/to/block";
|
||||
// TX signature in the block that should be looked at
|
||||
let txsig = &"";
|
||||
// 0-based index of instuction in the tx to try replaying
|
||||
let ix_index = 3;
|
||||
|
||||
if txsig.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(400_000);
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
let signer = context.users[0].key;
|
||||
|
||||
let known_accounts = [
|
||||
"ComputeBudget111111111111111111111111111111",
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
||||
"4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg",
|
||||
"11111111111111111111111111111111",
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| Pubkey::from_str(s).unwrap())
|
||||
.collect_vec();
|
||||
|
||||
// Load block, find tx
|
||||
let (slot, time, tx_bytes) = find_tx(block_file, txsig).unwrap();
|
||||
let tx: VersionedTransaction = bincode::deserialize(&tx_bytes).unwrap();
|
||||
|
||||
// Lookup ALTs so we can decompile
|
||||
let loaded_addresses: LoadedAddresses = tx
|
||||
.message
|
||||
.address_table_lookups()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|alt_lookup| {
|
||||
let alt_account = account_from_snapshot(snapshot_path, alt_lookup.account_key).unwrap();
|
||||
let alt = AddressLookupTable::deserialize(&alt_account.data()).unwrap();
|
||||
LoadedAddresses {
|
||||
readonly: alt_lookup
|
||||
.readonly_indexes
|
||||
.iter()
|
||||
.map(|i| alt.addresses[*i as usize])
|
||||
.collect_vec(),
|
||||
writable: alt_lookup
|
||||
.writable_indexes
|
||||
.iter()
|
||||
.map(|i| alt.addresses[*i as usize])
|
||||
.collect_vec(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let alt_loader = SimpleAddressLoader::Enabled(loaded_addresses);
|
||||
|
||||
// decompile instructions, looking up alts at the same time
|
||||
let sv_message = SanitizedVersionedMessage::try_from(tx.message).unwrap();
|
||||
let s_message = SanitizedMessage::try_new(sv_message, alt_loader).unwrap();
|
||||
let bix = &s_message.decompile_instructions()[ix_index];
|
||||
let ix = Instruction {
|
||||
program_id: *bix.program_id,
|
||||
accounts: bix
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|m| AccountMeta {
|
||||
pubkey: *m.pubkey,
|
||||
is_writable: m.is_writable,
|
||||
is_signer: m.is_signer,
|
||||
})
|
||||
.collect(),
|
||||
data: bix.data.to_vec(),
|
||||
};
|
||||
|
||||
// since we can't retain the original signer/blockhash, replace it
|
||||
let mut replaced_signers = vec![];
|
||||
let mut replaced_ix = ix.clone();
|
||||
for meta in &mut replaced_ix.accounts {
|
||||
if meta.is_signer {
|
||||
replaced_signers.push(meta.pubkey);
|
||||
meta.pubkey = signer.pubkey();
|
||||
}
|
||||
}
|
||||
|
||||
// Load all accounts, reporting missing ones, add found to context
|
||||
let mut missing_accounts = vec![];
|
||||
for pubkey in replaced_ix.accounts.iter().map(|m| m.pubkey) {
|
||||
if known_accounts.contains(&pubkey) || pubkey == signer.pubkey() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut account = match account_from_snapshot(snapshot_path, pubkey) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
println!("error reading account from snapshot: {pubkey}, error {e:?}");
|
||||
missing_accounts.push(pubkey);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Override where the previous signer was an owner
|
||||
if replaced_signers.contains(&account.owner) {
|
||||
account.owner = signer.pubkey();
|
||||
}
|
||||
|
||||
// Override mango account owners or delegates
|
||||
if let Ok(mut ma) = account.load_mut::<MangoAccountFixed>() {
|
||||
if replaced_signers.contains(&ma.owner) {
|
||||
ma.owner = signer.pubkey();
|
||||
}
|
||||
if replaced_signers.contains(&ma.delegate) {
|
||||
ma.delegate = signer.pubkey();
|
||||
}
|
||||
}
|
||||
|
||||
// Override token account owners
|
||||
if account.owner == spl_token::id() {
|
||||
if let Ok(mut ta) = spl_token::state::Account::unpack(&account.data) {
|
||||
if replaced_signers.contains(&ta.owner) {
|
||||
ta.owner = signer.pubkey();
|
||||
}
|
||||
spl_token::state::Account::pack(ta, &mut account.data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let mut program_test_context = solana.context.borrow_mut();
|
||||
program_test_context.set_account(&pubkey, &account.into());
|
||||
}
|
||||
if !missing_accounts.is_empty() {
|
||||
println!("There were account reading errors, maybe fetch them:");
|
||||
for a in &missing_accounts {
|
||||
println!("solana account {a} --output json -o {snapshot_path}/{a}.json");
|
||||
}
|
||||
anyhow::bail!("accounts were missing");
|
||||
}
|
||||
|
||||
// update slot/time to roughly match
|
||||
let mut clock = solana.clock().await;
|
||||
clock.slot = slot;
|
||||
clock.unix_timestamp = time;
|
||||
solana.set_clock(&clock);
|
||||
|
||||
// Send transaction
|
||||
solana
|
||||
.process_transaction(&[replaced_ix], Some(&[signer]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -36,35 +36,39 @@ impl SerumOrderPlacer {
|
|||
None
|
||||
}
|
||||
|
||||
fn bid_ix(
|
||||
&mut self,
|
||||
limit_price: f64,
|
||||
max_base: u64,
|
||||
taker: bool,
|
||||
) -> Serum3PlaceOrderInstruction {
|
||||
let client_order_id = self.inc_client_order_id();
|
||||
let fees = if taker { 0.0004 } else { 0.0 };
|
||||
Serum3PlaceOrderInstruction {
|
||||
side: Serum3Side::Bid,
|
||||
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
|
||||
max_base_qty: max_base / 100, // in base lot (100)
|
||||
// 4 bps taker fees added in
|
||||
max_native_quote_qty_including_fees: (limit_price * (max_base as f64) * (1.0 + fees))
|
||||
.ceil() as u64,
|
||||
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
|
||||
order_type: Serum3OrderType::Limit,
|
||||
client_order_id,
|
||||
limit: 10,
|
||||
account: self.account,
|
||||
owner: self.owner,
|
||||
serum_market: self.serum_market,
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_bid(
|
||||
&mut self,
|
||||
limit_price: f64,
|
||||
max_base: u64,
|
||||
taker: bool,
|
||||
) -> Result<mango_v4::accounts::Serum3PlaceOrder, TransportError> {
|
||||
let client_order_id = self.inc_client_order_id();
|
||||
let fees = if taker { 0.0004 } else { 0.0 };
|
||||
send_tx(
|
||||
&self.solana,
|
||||
Serum3PlaceOrderInstruction {
|
||||
side: Serum3Side::Bid,
|
||||
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
|
||||
max_base_qty: max_base / 100, // in base lot (100)
|
||||
// 4 bps taker fees added in
|
||||
max_native_quote_qty_including_fees: (limit_price
|
||||
* (max_base as f64)
|
||||
* (1.0 + fees))
|
||||
.ceil() as u64,
|
||||
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
|
||||
order_type: Serum3OrderType::Limit,
|
||||
client_order_id,
|
||||
limit: 10,
|
||||
account: self.account,
|
||||
owner: self.owner,
|
||||
serum_market: self.serum_market,
|
||||
},
|
||||
)
|
||||
.await
|
||||
let ix = self.bid_ix(limit_price, max_base, taker);
|
||||
send_tx(&self.solana, ix).await
|
||||
}
|
||||
|
||||
async fn bid_maker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
|
@ -579,7 +583,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
|||
.get_account::<Bank>(quote_bank)
|
||||
.await
|
||||
.collected_fees_native;
|
||||
assert!(assert_equal(quote_fees2 - quote_fees1, 0.0, 0.1));
|
||||
assert_eq_fixed_f64!(quote_fees2 - quote_fees1, 0.0, 0.1);
|
||||
|
||||
// check account2 balances too
|
||||
context
|
||||
|
@ -610,11 +614,11 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
|||
.get_account::<Bank>(quote_bank)
|
||||
.await
|
||||
.collected_fees_native;
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
quote_fees3 - quote_fees1,
|
||||
loan_origination_fee(fill_amount - deposit_amount) as f64,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
order_placer.settle().await;
|
||||
|
||||
|
@ -623,11 +627,11 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
|||
.get_account::<Bank>(quote_bank)
|
||||
.await
|
||||
.collected_fees_native;
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
quote_fees4 - quote_fees3,
|
||||
serum_fee(fill_amount) as f64,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert_eq!(
|
||||
|
@ -720,11 +724,11 @@ async fn test_serum_settle_v1() -> Result<(), TransportError> {
|
|||
.get_account::<Bank>(quote_bank)
|
||||
.await
|
||||
.collected_fees_native;
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
quote_fees_end - quote_fees_start,
|
||||
(lof + serum_referrer_fee(amount)) as f64,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -817,11 +821,11 @@ async fn test_serum_settle_v2_to_dao() -> Result<(), TransportError> {
|
|||
.get_account::<Bank>(quote_bank)
|
||||
.await
|
||||
.collected_fees_native;
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
quote_fees_end - quote_fees_start,
|
||||
(lof + serum_referrer_fee(amount)) as f64,
|
||||
0.1
|
||||
));
|
||||
);
|
||||
|
||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert_eq!(
|
||||
|
@ -913,11 +917,7 @@ async fn test_serum_settle_v2_to_account() -> Result<(), TransportError> {
|
|||
.get_account::<Bank>(quote_bank)
|
||||
.await
|
||||
.collected_fees_native;
|
||||
assert!(assert_equal(
|
||||
quote_fees_end - quote_fees_start,
|
||||
lof as f64,
|
||||
0.1
|
||||
));
|
||||
assert_eq_fixed_f64!(quote_fees_end - quote_fees_start, lof as f64, 0.1);
|
||||
|
||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert_eq!(account_data.buyback_fees_accrued_current, 0);
|
||||
|
@ -1029,7 +1029,7 @@ async fn test_serum_reduce_only_deposits1() -> Result<(), TransportError> {
|
|||
#[tokio::test]
|
||||
async fn test_serum_reduce_only_deposits2() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
test_builder.test().set_compute_max_units(97_000); // Serum3PlaceOrder needs 95.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -1952,6 +1952,71 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum_skip_bank() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
//
|
||||
// SETUP: Create a group, accounts, market etc
|
||||
//
|
||||
let deposit_amount = 5000;
|
||||
let CommonSetup {
|
||||
group_with_tokens,
|
||||
mut order_placer,
|
||||
..
|
||||
} = common_setup(&context, deposit_amount).await;
|
||||
let tokens = group_with_tokens.tokens;
|
||||
|
||||
//
|
||||
// TESTS
|
||||
//
|
||||
|
||||
// verify generally good
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: order_placer.bid_ix(1.0, 100, false),
|
||||
skip_banks: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can skip uninvolved token
|
||||
send_tx(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: order_placer.bid_ix(1.0, 100, false),
|
||||
skip_banks: vec![tokens[2].bank],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can't skip base or quote token
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: order_placer.bid_ix(1.0, 100, false),
|
||||
skip_banks: vec![tokens[0].bank],
|
||||
},
|
||||
MangoError::TokenPositionDoesNotExist
|
||||
);
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
HealthAccountSkipping {
|
||||
inner: order_placer.bid_ix(1.0, 100, false),
|
||||
skip_banks: vec![tokens[1].bank],
|
||||
},
|
||||
MangoError::TokenPositionDoesNotExist
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CommonSetup {
|
||||
group_with_tokens: GroupWithTokens,
|
||||
serum_market_cookie: SpotMarketCookie,
|
||||
|
|
|
@ -321,7 +321,7 @@ async fn test_fallback_oracle_withdraw() -> Result<(), TransportError> {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_clmm_fallback_oracle() -> Result<(), TransportError> {
|
||||
async fn test_orca_fallback_oracle() -> Result<(), TransportError> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
@ -340,6 +340,237 @@ async fn test_clmm_fallback_oracle() -> Result<(), TransportError> {
|
|||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
||||
"FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH",
|
||||
),
|
||||
(
|
||||
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
|
||||
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
|
||||
),
|
||||
];
|
||||
|
||||
let fallback_oracle = Pubkey::from_str(fixtures[0].0).unwrap();
|
||||
let pyth_usd_oracle = Pubkey::from_str(fixtures[1].0).unwrap();
|
||||
let wrong_fallback_oracle = Pubkey::from_str(fixtures[2].0).unwrap();
|
||||
|
||||
// setup pyth and clmm accounts
|
||||
for fixture in fixtures {
|
||||
let filename = format!("resources/test/{}.bin", fixture.0);
|
||||
let data = read_file(find_file(&filename).unwrap());
|
||||
let mut account =
|
||||
AccountSharedData::new(u64::MAX, data.len(), &Pubkey::from_str(fixture.1).unwrap());
|
||||
account.set_data(data);
|
||||
let mut program_test_context = solana.context.borrow_mut();
|
||||
program_test_context.set_account(&Pubkey::from_str(fixture.0).unwrap(), &account);
|
||||
}
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..4];
|
||||
let payer_token_accounts = &context.users[1].token_accounts[0..3];
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// add a fallback oracle
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[2].pubkey,
|
||||
fallback_oracle,
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
set_fallback_oracle: true,
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bank_data: Bank = solana.get_account(tokens[2].bank).await;
|
||||
assert!(bank_data.fallback_oracle == fallback_oracle);
|
||||
|
||||
// fill vaults, so we can borrow
|
||||
let _vault_account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
2,
|
||||
&context.users[1],
|
||||
mints,
|
||||
100_000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create account with token3 of deposits
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&[mints[2]],
|
||||
10_000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Adjust oracle prices to match CLMM
|
||||
for i in 0..3 {
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetTestInstruction {
|
||||
oracle: tokens[i].oracle,
|
||||
group,
|
||||
mint: mints[i].pubkey,
|
||||
admin,
|
||||
price: 0.06300727055072872,
|
||||
last_update_slot: 0,
|
||||
deviation: 0.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Create some token1 borrows
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 100,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make oracle invalid by increasing deviation
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetTestInstruction {
|
||||
oracle: tokens[2].oracle,
|
||||
group,
|
||||
mint: mints[2].pubkey,
|
||||
admin,
|
||||
price: 0.06300727055072872,
|
||||
last_update_slot: 0,
|
||||
deviation: 100.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_withdraw_ix = TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_token_accounts[2],
|
||||
bank_index: 0,
|
||||
};
|
||||
|
||||
// Verify that withdrawing collateral won't work
|
||||
assert!(send_tx(solana, token_withdraw_ix.clone()).await.is_err());
|
||||
|
||||
// Send txn with a fallback oracle in the remaining accounts, but no pyth USD feed
|
||||
let fallback_oracle_meta = AccountMeta {
|
||||
pubkey: fallback_oracle,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
};
|
||||
assert!(send_tx_with_extra_accounts(
|
||||
solana,
|
||||
token_withdraw_ix.clone(),
|
||||
vec![fallback_oracle_meta.clone()]
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
.is_err());
|
||||
|
||||
// add wrong_fallback_oracle for a different token
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[3].pubkey,
|
||||
fallback_oracle: wrong_fallback_oracle,
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
set_fallback_oracle: true,
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send txn with a the wrong fallback oracle
|
||||
let wrong_fallback_meta = AccountMeta {
|
||||
pubkey: wrong_fallback_oracle,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
};
|
||||
assert!(send_tx_with_extra_accounts(
|
||||
solana,
|
||||
token_withdraw_ix.clone(),
|
||||
vec![wrong_fallback_meta.clone()]
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
.is_err());
|
||||
|
||||
// Finally send txn with a fallback oracle and pyth USD feed
|
||||
let pyth_usd_oracle_meta = AccountMeta {
|
||||
pubkey: pyth_usd_oracle,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
};
|
||||
send_tx_with_extra_accounts(
|
||||
solana,
|
||||
token_withdraw_ix,
|
||||
vec![fallback_oracle_meta, pyth_usd_oracle_meta],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_raydium_fallback_oracle() -> Result<(), TransportError> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(150_000); // bad oracles log a lot
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let fixtures = vec![
|
||||
(
|
||||
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
|
||||
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
|
||||
),
|
||||
(
|
||||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
||||
"FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH",
|
||||
),
|
||||
];
|
||||
|
||||
let fallback_oracle = Pubkey::from_str(fixtures[0].0).unwrap();
|
||||
|
|
|
@ -2,8 +2,6 @@ use super::*;
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
|
||||
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
|
||||
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -263,17 +261,17 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
|
|||
|
||||
let liqee_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let liqee_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f_f(
|
||||
assert_eq_f64!(
|
||||
liqee_quote,
|
||||
deposit_amount + 42.0, // roughly 50 / (1.1 * 1.1)
|
||||
0.01
|
||||
));
|
||||
assert!(assert_equal_f_f(liqee_base, deposit_amount - 50.0, 0.01));
|
||||
);
|
||||
assert_eq_f64!(liqee_base, deposit_amount - 50.0, 0.01);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f_f(liqor_quote, deposit_amount - 42.0, 0.01));
|
||||
assert!(assert_equal_f_f(liqor_base, deposit_amount + 44.0, 0.01)); // roughly 42*1.1*0.95
|
||||
assert_eq_f64!(liqor_quote, deposit_amount - 42.0, 0.01);
|
||||
assert_eq_f64!(liqor_base, deposit_amount + 44.0, 0.01); // roughly 42*1.1*0.95
|
||||
|
||||
//
|
||||
// TEST: requiring a too-high min buy token execution makes it fail
|
||||
|
@ -315,13 +313,13 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
|
|||
|
||||
let liqee_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let liqee_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f_f(liqee_quote, deposit_amount + 84.0, 0.01));
|
||||
assert!(assert_equal_f_f(liqee_base, deposit_amount - 100.0, 0.01));
|
||||
assert_eq_f64!(liqee_quote, deposit_amount + 84.0, 0.01);
|
||||
assert_eq_f64!(liqee_base, deposit_amount - 100.0, 0.01);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f_f(liqor_quote, deposit_amount - 84.0, 0.01));
|
||||
assert!(assert_equal_f_f(liqor_base, deposit_amount + 88.0, 0.01));
|
||||
assert_eq_f64!(liqor_quote, deposit_amount - 84.0, 0.01);
|
||||
assert_eq_f64!(liqor_base, deposit_amount + 88.0, 0.01);
|
||||
|
||||
let account_data = get_mango_account(solana, account).await;
|
||||
assert!(!account_data
|
||||
|
@ -334,8 +332,6 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportError> {
|
||||
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
|
||||
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -460,7 +456,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
|||
// TEST: Can't take an auction at any price when it's not started yet
|
||||
//
|
||||
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
|
@ -472,12 +468,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
|||
min_buy_token: 0,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapNotStarted.into(),
|
||||
"tcs should not be started yet".to_string(),
|
||||
MangoError::TokenConditionalSwapNotStarted
|
||||
);
|
||||
|
||||
//
|
||||
|
@ -507,21 +498,13 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
|||
|
||||
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_quote,
|
||||
account_quote_expected,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_base,
|
||||
account_base_expected,
|
||||
0.1
|
||||
));
|
||||
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
|
||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
||||
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||
|
||||
//
|
||||
// TEST: Stays at end price after end and before expiry
|
||||
|
@ -550,27 +533,19 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
|||
|
||||
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_quote,
|
||||
account_quote_expected,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_base,
|
||||
account_base_expected,
|
||||
0.1
|
||||
));
|
||||
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
|
||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
||||
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||
|
||||
//
|
||||
// TEST: Can't take when expired
|
||||
//
|
||||
solana.set_clock_timestamp(initial_time + 22).await;
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
|
@ -582,12 +557,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
|||
min_buy_token: 1,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapExpired.into(),
|
||||
"tcs should be expired".to_string(),
|
||||
MangoError::TokenConditionalSwapExpired
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
@ -595,8 +565,6 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportError> {
|
||||
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
|
||||
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -720,7 +688,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
//
|
||||
|
||||
set_bank_stub_oracle_price(solana, group, &base_token, admin, 10.0).await;
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
|
@ -732,15 +700,10 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
min_buy_token: 0,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapNotStarted.into(),
|
||||
"not started yet".to_string(),
|
||||
MangoError::TokenConditionalSwapNotStarted
|
||||
);
|
||||
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapStartInstruction {
|
||||
liqee: account,
|
||||
|
@ -748,19 +711,14 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
liqor_owner: owner,
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapPriceNotInRange.into(),
|
||||
"price not in range".to_string(),
|
||||
MangoError::TokenConditionalSwapPriceNotInRange
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Cannot trigger without start
|
||||
//
|
||||
set_bank_stub_oracle_price(solana, group, &base_token, admin, 1.0).await;
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
|
@ -772,12 +730,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
min_buy_token: 1,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapNotStarted.into(),
|
||||
"not started yet".to_string(),
|
||||
MangoError::TokenConditionalSwapNotStarted
|
||||
);
|
||||
|
||||
send_tx(
|
||||
|
@ -815,21 +768,13 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
|
||||
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_quote,
|
||||
account_quote_expected,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_base,
|
||||
account_base_expected,
|
||||
0.1
|
||||
));
|
||||
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
|
||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
||||
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||
|
||||
let account_data = get_mango_account(solana, account).await;
|
||||
let tcs = account_data
|
||||
|
@ -866,21 +811,13 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
|
||||
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_quote,
|
||||
account_quote_expected,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_base,
|
||||
account_base_expected,
|
||||
0.1
|
||||
));
|
||||
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
|
||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
||||
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||
|
||||
//
|
||||
// TEST: Premium stops at max increases
|
||||
|
@ -910,21 +847,13 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
|
||||
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_quote,
|
||||
account_quote_expected,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal_f64_f64(
|
||||
account_base,
|
||||
account_base_expected,
|
||||
0.1
|
||||
));
|
||||
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||
|
||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
|
||||
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
|
||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
||||
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||
|
||||
//
|
||||
// SETUP: make another premium auction to test starting
|
||||
|
@ -954,7 +883,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
// TEST: Can't start if oracle not in range
|
||||
//
|
||||
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapStartInstruction {
|
||||
liqee: account,
|
||||
|
@ -962,12 +891,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
liqor_owner: owner,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapPriceNotInRange.into(),
|
||||
"price is not in range".to_string(),
|
||||
MangoError::TokenConditionalSwapPriceNotInRange
|
||||
);
|
||||
|
||||
//
|
||||
|
@ -998,7 +922,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
// TEST: Can't start a second time
|
||||
//
|
||||
|
||||
let res = send_tx(
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapStartInstruction {
|
||||
liqee: account,
|
||||
|
@ -1006,12 +930,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
liqor_owner: owner,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_mango_error(
|
||||
&res,
|
||||
MangoError::TokenConditionalSwapAlreadyStarted.into(),
|
||||
"already started".to_string(),
|
||||
MangoError::TokenConditionalSwapAlreadyStarted
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
@ -1019,8 +938,6 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_token_conditional_swap_deposit_limit() -> Result<(), TransportError> {
|
||||
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
|
||||
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
|
|
@ -78,22 +78,22 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
|
|||
let interest_change = 5000.0 * (dynamic_rate + loan_fee_rate) * diff_ts / year;
|
||||
let fee_change = 5000.0 * loan_fee_rate * diff_ts / year;
|
||||
|
||||
assert!(assert_equal(
|
||||
assert_eq_fixed_f64!(
|
||||
bank_after.native_borrows() - bank_before.native_borrows(),
|
||||
interest_change,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
bank_after.native_deposits() - bank_before.native_deposits(),
|
||||
interest_change,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
);
|
||||
assert_eq_fixed_f64!(
|
||||
bank_after.collected_fees_native - bank_before.collected_fees_native,
|
||||
fee_change,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(bank_after.avg_utilization, utilization, 0.01));
|
||||
);
|
||||
assert_eq_fixed_f64!(bank_after.avg_utilization, utilization, 0.01);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -140,19 +140,11 @@ async fn test_token_rates_migrate() -> Result<(), TransportError> {
|
|||
|
||||
let bank_after = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
|
||||
assert!(assert_equal_fixed_f64(bank_after.rate0, 0.07 / 3.0, 0.0001));
|
||||
assert!(assert_equal_fixed_f64(bank_after.rate1, 0.9 / 3.0, 0.0001));
|
||||
assert!(assert_equal_fixed_f64(bank_after.max_rate, 0.5, 0.0001));
|
||||
assert!(assert_equal_f64_f64(
|
||||
bank_after.interest_curve_scaling,
|
||||
3.0,
|
||||
0.0001
|
||||
));
|
||||
assert!(assert_equal_f64_f64(
|
||||
bank_after.interest_target_utilization as f64,
|
||||
0.4,
|
||||
0.0001
|
||||
));
|
||||
assert_eq_fixed_f64!(bank_after.rate0, 0.07 / 3.0, 0.0001);
|
||||
assert_eq_fixed_f64!(bank_after.rate1, 0.9 / 3.0, 0.0001);
|
||||
assert_eq_fixed_f64!(bank_after.max_rate, 0.5, 0.0001);
|
||||
assert_eq_f64!(bank_after.interest_curve_scaling, 3.0, 0.0001);
|
||||
assert_eq_f64!(bank_after.interest_target_utilization as f64, 0.4, 0.0001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,6 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use bytemuck::{bytes_of, Contiguous};
|
||||
use fixed::types::I80F48;
|
||||
use solana_program::instruction::InstructionError;
|
||||
use solana_program::program_error::ProgramError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
@ -97,18 +96,22 @@ pub fn assert_mango_error<T>(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn assert_equal_fixed_f64(value: I80F48, expected: f64, max_error: f64) -> bool {
|
||||
let ok = (value.to_num::<f64>() - expected).abs() < max_error;
|
||||
if !ok {
|
||||
println!("comparison failed: value: {value}, expected: {expected}");
|
||||
}
|
||||
ok
|
||||
#[macro_export]
|
||||
macro_rules! assert_eq_f64 {
|
||||
($value:expr, $expected:expr, $max_error:expr $(,)?) => {
|
||||
let value = $value;
|
||||
let expected = $expected;
|
||||
let ok = (value - expected).abs() < $max_error;
|
||||
if !ok {
|
||||
println!("comparison failed: value: {value}, expected: {expected}");
|
||||
}
|
||||
assert!(ok);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn assert_equal_f64_f64(value: f64, expected: f64, max_error: f64) -> bool {
|
||||
let ok = (value - expected).abs() < max_error;
|
||||
if !ok {
|
||||
println!("comparison failed: value: {value}, expected: {expected}");
|
||||
}
|
||||
ok
|
||||
#[macro_export]
|
||||
macro_rules! assert_eq_fixed_f64 {
|
||||
($value:expr, $expected:expr, $max_error:expr $(,)?) => {
|
||||
assert_eq_f64!($value.to_num::<f64>(), $expected, $max_error);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
### Set environment variables
|
||||
|
||||
```
|
||||
CLUSTER=devnet
|
||||
CLUSTER_URL=https://mango.devnet.rpcpool.com/<token>
|
||||
PAYER_KEYPAIR=~/.config/solana/mb-liqtest.json
|
||||
# Adjust this to a free group
|
||||
GROUP_NUM=200
|
||||
```
|
||||
|
||||
### Get devnet SOL
|
||||
|
||||
The scripts need a lot of SOL for mint, market, group and account creation.
|
||||
There's ample available, best to ask around.
|
||||
|
||||
### Create tokens and markets
|
||||
|
||||
This is one-time setup:
|
||||
|
||||
```
|
||||
yarn ts-node ts/client/scripts/liqtest/liqtest-create-tokens-and-markets.ts
|
||||
```
|
||||
|
||||
It'll emit some MINTS=... and SERUM_MARKETS=.. env vars, set those, all further
|
||||
commands will use them.
|
||||
|
||||
### Make a group
|
||||
|
||||
```
|
||||
yarn ts-node ts/client/scripts/liqtest/liqtest-create-group.ts
|
||||
```
|
||||
|
||||
Groups can be reused a lot, but sometimes closing them may be necessary
|
||||
|
||||
```
|
||||
yarn ts-node ts/client/scripts/liqtest/liqtest-close-group.ts
|
||||
```
|
||||
|
||||
Preferably close all mango accounts first.
|
||||
|
||||
### Create candidate mango accounts
|
||||
|
||||
```
|
||||
yarn ts-node ts/client/scripts/liqtest/liqtest-make-candidates.ts
|
||||
```
|
||||
|
||||
This creates a bunch of to-be-liquidated accounts as well as a LIQOR account.
|
||||
|
||||
### Liquidate
|
||||
|
||||
Run the liquidator on the group with the liqor account.
|
||||
|
||||
Since devnet doesn't have any jupiter, run with
|
||||
|
||||
```
|
||||
JUPITER_VERSION=mock
|
||||
TCS_MODE=borrow-buy
|
||||
REBALANCE=false
|
||||
```
|
||||
|
||||
### Settle and close all open mango accounts
|
||||
|
||||
At any point, to reset by closing all accounts:
|
||||
|
||||
```
|
||||
yarn ts-node ts/client/scripts/liqtest/liqtest-settle-and-close-all.ts
|
||||
```
|
|
@ -51,7 +51,7 @@ async function main() {
|
|||
market.serumMarketExternal,
|
||||
);
|
||||
console.log(
|
||||
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig.signature}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ async function main() {
|
|||
for (const market of group.perpMarketsMapByMarketIndex.values()) {
|
||||
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
|
||||
console.log(
|
||||
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig.signature}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ async function main() {
|
|||
for (const banks of group.banksMapByMint.values()) {
|
||||
sig = await client.tokenDeregister(group, banks[0].mint);
|
||||
console.log(
|
||||
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig.signature}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -76,13 +76,15 @@ async function main() {
|
|||
for (const stubOracle of stubOracles) {
|
||||
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
||||
console.log(
|
||||
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig.signature}`,
|
||||
);
|
||||
}
|
||||
|
||||
// finally, close the group
|
||||
sig = await client.groupClose(group);
|
||||
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
|
||||
console.log(
|
||||
`Closed group, sig https://explorer.solana.com/tx/${sig.signature}`,
|
||||
);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
|
|
|
@ -330,12 +330,12 @@ async function createAndPopulateAlt(
|
|||
});
|
||||
let sig = await client.sendAndConfirmTransaction([createIx[0]]);
|
||||
console.log(
|
||||
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`,
|
||||
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig.signature}`,
|
||||
);
|
||||
|
||||
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}`);
|
||||
console.log(`...https://explorer.solana.com/tx/${sig.signature}`);
|
||||
|
||||
group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
} catch (error) {
|
||||
|
@ -366,7 +366,7 @@ async function createAndPopulateAlt(
|
|||
addresses,
|
||||
});
|
||||
const sig = await client.sendAndConfirmTransaction([extendIx]);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}`);
|
||||
console.log(`https://explorer.solana.com/tx/${sig.signature}`);
|
||||
}
|
||||
|
||||
// Extend using mango v4 relevant pub keys
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import {
|
||||
HealthType,
|
||||
MangoAccount,
|
||||
TokenPosition,
|
||||
TokenPositionDto,
|
||||
} from './mangoAccount';
|
||||
import BN from 'bn.js';
|
||||
import { Bank, TokenIndex } from './bank';
|
||||
import { deepClone, toNative, toUiDecimals } from '../utils';
|
||||
import { expect } from 'chai';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { Group } from './group';
|
||||
import { HealthCache } from './healthCache';
|
||||
import { assert } from 'console';
|
||||
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { deepClone, toNative, toUiDecimals } from '../utils';
|
||||
import { Bank, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
import { MangoAccount, TokenPosition } from './mangoAccount';
|
||||
|
||||
describe('Mango Account', () => {
|
||||
const mangoAccount = new MangoAccount(
|
||||
|
@ -32,6 +26,7 @@ describe('Mango Account', () => {
|
|||
new BN(0),
|
||||
new BN(0),
|
||||
0,
|
||||
0,
|
||||
new BN(0),
|
||||
[],
|
||||
[],
|
||||
|
@ -106,6 +101,7 @@ describe('maxWithdraw', () => {
|
|||
new BN(0),
|
||||
new BN(0),
|
||||
0,
|
||||
0,
|
||||
new BN(0),
|
||||
[],
|
||||
[],
|
||||
|
|
|
@ -51,6 +51,7 @@ export class MangoAccount {
|
|||
buybackFeesAccruedCurrent: BN;
|
||||
buybackFeesAccruedPrevious: BN;
|
||||
buybackFeesExpiryTimestamp: BN;
|
||||
sequenceNumber: number;
|
||||
headerVersion: number;
|
||||
tokens: unknown;
|
||||
serum3: unknown;
|
||||
|
@ -76,6 +77,7 @@ export class MangoAccount {
|
|||
obj.buybackFeesAccruedCurrent,
|
||||
obj.buybackFeesAccruedPrevious,
|
||||
obj.buybackFeesExpiryTimestamp,
|
||||
obj.sequenceNumber,
|
||||
obj.headerVersion,
|
||||
obj.lastCollateralFeeCharge,
|
||||
obj.tokens as TokenPositionDto[],
|
||||
|
@ -103,6 +105,7 @@ export class MangoAccount {
|
|||
public buybackFeesAccruedCurrent: BN,
|
||||
public buybackFeesAccruedPrevious: BN,
|
||||
public buybackFeesExpiryTimestamp: BN,
|
||||
public sequenceNumber: number,
|
||||
public headerVersion: number,
|
||||
public lastCollateralFeeCharge: BN,
|
||||
tokens: TokenPositionDto[],
|
||||
|
|
|
@ -83,7 +83,7 @@ import {
|
|||
import { Id } from './ids';
|
||||
import { IDL, MangoV4 } from './mango_v4';
|
||||
import { I80F48 } from './numbers/I80F48';
|
||||
import { FlashLoanType, OracleConfigParams } from './types';
|
||||
import { FlashLoanType, HealthCheckKind, OracleConfigParams } from './types';
|
||||
import {
|
||||
I64_MAX_BN,
|
||||
U64_MAX_BN,
|
||||
|
@ -1056,6 +1056,50 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async sequenceCheckIx(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
): Promise<TransactionInstruction> {
|
||||
return await this.program.methods
|
||||
.sequenceCheck(mangoAccount.sequenceNumber)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
public async healthCheckIx(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
minHealthValue: number,
|
||||
checkKind: HealthCheckKind,
|
||||
): Promise<TransactionInstruction> {
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
await this.buildHealthRemainingAccounts(
|
||||
group,
|
||||
[mangoAccount],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
return await this.program.methods
|
||||
.healthCheck(minHealthValue, checkKind)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
})
|
||||
.remainingAccounts(
|
||||
healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
|
||||
),
|
||||
)
|
||||
.instruction();
|
||||
}
|
||||
|
||||
public async getMangoAccount(
|
||||
mangoAccountPk: PublicKey,
|
||||
loadSerum3Oo = false,
|
||||
|
|
|
@ -310,6 +310,8 @@ export interface IxGateParams {
|
|||
TokenConditionalSwapCreateLinearAuction: boolean;
|
||||
Serum3PlaceOrderV2: boolean;
|
||||
TokenForceWithdraw: boolean;
|
||||
SequenceCheck: boolean;
|
||||
HealthCheck: boolean;
|
||||
}
|
||||
|
||||
// Default with all ixs enabled, use with buildIxGate
|
||||
|
@ -390,6 +392,8 @@ export const TrueIxGateParams: IxGateParams = {
|
|||
TokenConditionalSwapCreateLinearAuction: true,
|
||||
Serum3PlaceOrderV2: true,
|
||||
TokenForceWithdraw: true,
|
||||
SequenceCheck: true,
|
||||
HealthCheck: true,
|
||||
};
|
||||
|
||||
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
||||
|
@ -480,6 +484,8 @@ export function buildIxGate(p: IxGateParams): BN {
|
|||
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
||||
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
||||
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
|
||||
toggleIx(ixGate, p, 'SequenceCheck', 73);
|
||||
toggleIx(ixGate, p, 'HealthCheck', 74);
|
||||
|
||||
return ixGate;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export type MangoV4 = {
|
||||
"version": "0.23.0",
|
||||
"version": "0.24.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -1760,6 +1760,66 @@ export type MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sequenceCheck",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "expectedSequenceNumber",
|
||||
"type": "u8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "healthCheck",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "minHealthValue",
|
||||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "checkKind",
|
||||
"type": {
|
||||
"defined": "HealthCheckKind"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stubOracleCreate",
|
||||
"accounts": [
|
||||
|
@ -7871,13 +7931,8 @@ export type MangoV4 = {
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1
|
||||
]
|
||||
}
|
||||
"name": "sequenceNumber",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "netDeposits",
|
||||
|
@ -9669,13 +9724,8 @@ export type MangoV4 = {
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1
|
||||
]
|
||||
}
|
||||
"name": "sequenceNumber",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "netDeposits",
|
||||
|
@ -10654,6 +10704,32 @@ export type MangoV4 = {
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "HealthCheckKind",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Maint"
|
||||
},
|
||||
{
|
||||
"name": "Init"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEnd"
|
||||
},
|
||||
{
|
||||
"name": "MaintRatio"
|
||||
},
|
||||
{
|
||||
"name": "InitRatio"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEndRatio"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Serum3SelfTradeBehavior",
|
||||
"docs": [
|
||||
|
@ -11008,6 +11084,12 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
},
|
||||
{
|
||||
"name": "SequenceCheck"
|
||||
},
|
||||
{
|
||||
"name": "HealthCheck"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -11048,6 +11130,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "OrcaCLMM"
|
||||
},
|
||||
{
|
||||
"name": "RaydiumCLMM"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -14347,12 +14432,27 @@ export type MangoV4 = {
|
|||
"code": 6069,
|
||||
"name": "TokenAssetLiquidationDisabled",
|
||||
"msg": "the asset does not allow liquidation"
|
||||
},
|
||||
{
|
||||
"code": 6070,
|
||||
"name": "BorrowsRequireHealthAccountBank",
|
||||
"msg": "for borrows the bank must be in the health account list"
|
||||
},
|
||||
{
|
||||
"code": 6071,
|
||||
"name": "InvalidSequenceNumber",
|
||||
"msg": "invalid sequence number"
|
||||
},
|
||||
{
|
||||
"code": 6072,
|
||||
"name": "InvalidHealth",
|
||||
"msg": "invalid health"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const IDL: MangoV4 = {
|
||||
"version": "0.23.0",
|
||||
"version": "0.24.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -16113,6 +16213,66 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sequenceCheck",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "expectedSequenceNumber",
|
||||
"type": "u8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "healthCheck",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "minHealthValue",
|
||||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "checkKind",
|
||||
"type": {
|
||||
"defined": "HealthCheckKind"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stubOracleCreate",
|
||||
"accounts": [
|
||||
|
@ -22224,13 +22384,8 @@ export const IDL: MangoV4 = {
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1
|
||||
]
|
||||
}
|
||||
"name": "sequenceNumber",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "netDeposits",
|
||||
|
@ -24022,13 +24177,8 @@ export const IDL: MangoV4 = {
|
|||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
1
|
||||
]
|
||||
}
|
||||
"name": "sequenceNumber",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "netDeposits",
|
||||
|
@ -25007,6 +25157,32 @@ export const IDL: MangoV4 = {
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "HealthCheckKind",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "Maint"
|
||||
},
|
||||
{
|
||||
"name": "Init"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEnd"
|
||||
},
|
||||
{
|
||||
"name": "MaintRatio"
|
||||
},
|
||||
{
|
||||
"name": "InitRatio"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEndRatio"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Serum3SelfTradeBehavior",
|
||||
"docs": [
|
||||
|
@ -25361,6 +25537,12 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
},
|
||||
{
|
||||
"name": "SequenceCheck"
|
||||
},
|
||||
{
|
||||
"name": "HealthCheck"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -25401,6 +25583,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "OrcaCLMM"
|
||||
},
|
||||
{
|
||||
"name": "RaydiumCLMM"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -28700,6 +28885,21 @@ export const IDL: MangoV4 = {
|
|||
"code": 6069,
|
||||
"name": "TokenAssetLiquidationDisabled",
|
||||
"msg": "the asset does not allow liquidation"
|
||||
},
|
||||
{
|
||||
"code": 6070,
|
||||
"name": "BorrowsRequireHealthAccountBank",
|
||||
"msg": "for borrows the bank must be in the health account list"
|
||||
},
|
||||
{
|
||||
"code": 6071,
|
||||
"name": "InvalidSequenceNumber",
|
||||
"msg": "invalid sequence number"
|
||||
},
|
||||
{
|
||||
"code": 6072,
|
||||
"name": "InvalidHealth",
|
||||
"msg": "invalid health"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -18,6 +18,23 @@ export namespace FlashLoanType {
|
|||
export const swapWithoutFee = { swapWithoutFee: {} };
|
||||
}
|
||||
|
||||
export type HealthCheckKind =
|
||||
| { maint: Record<string, never> }
|
||||
| { init: Record<string, never> }
|
||||
| { liquidationEnd: Record<string, never> }
|
||||
| { maintRatio: Record<string, never> }
|
||||
| { initRatio: Record<string, never> }
|
||||
| { liquidationEndRatio: Record<string, never> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace HealthCheckKind {
|
||||
export const maint = { maint: {} };
|
||||
export const init = { init: {} };
|
||||
export const liquidationEnd = { liquidationEnd: {} };
|
||||
export const maintRatio = { maintRatio: {} };
|
||||
export const initRatio = { initRatio: {} };
|
||||
export const liquidationEndRatio = { liquidationEndRatio: {} };
|
||||
}
|
||||
|
||||
export class InterestRateParams {
|
||||
util0: number;
|
||||
rate0: number;
|
||||
|
|
Loading…
Reference in New Issue