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
|
## 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)
|
- 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)
|
- Flash loan: Add a "swap without flash loan fees" option (#882)
|
||||||
- Cleanup, tests and minor (#878, #875, #854, #838, #895)
|
- Cleanup, tests and minor (#878, #875, #854, #838, #895)
|
||||||
|
|
||||||
## mainnet
|
|
||||||
|
|
||||||
### v0.22.0, 2024-3-3
|
### v0.22.0, 2024-3-3
|
||||||
|
|
||||||
Deployment: Mar 3, 2024 at 23:52:08 Central European Standard Time, https://explorer.solana.com/tx/3MpEMU12Pv7RpSnwfShoM9sbyr41KAEeJFCVx9ypkq8nuK8Q5vm7CRLkdhH3u91yQ4k44a32armZHaoYguX6NqsY
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
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]]
|
[[package]]
|
||||||
name = "headers"
|
name = "headers"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -3367,15 +3381,17 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mango-v4"
|
name = "mango-v4"
|
||||||
version = "0.23.0"
|
version = "0.24.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anchor-lang",
|
"anchor-lang",
|
||||||
"anchor-spl",
|
"anchor-spl",
|
||||||
|
"anyhow",
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bincode",
|
"bincode",
|
||||||
"borsh 0.10.3",
|
"borsh 0.10.3",
|
||||||
|
"bs58 0.5.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"default-env",
|
"default-env",
|
||||||
"derivative",
|
"derivative",
|
||||||
|
@ -3391,6 +3407,7 @@ dependencies = [
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serum_dex 0.5.10 (git+https://github.com/openbook-dex/program.git)",
|
"serum_dex 0.5.10 (git+https://github.com/openbook-dex/program.git)",
|
||||||
"solana-address-lookup-table-program",
|
"solana-address-lookup-table-program",
|
||||||
"solana-logger",
|
"solana-logger",
|
||||||
|
@ -3522,6 +3539,7 @@ dependencies = [
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"hdrhistogram",
|
||||||
"itertools",
|
"itertools",
|
||||||
"jemallocator",
|
"jemallocator",
|
||||||
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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
|
- 4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg is the address of the Mango v4 Program
|
||||||
- FP4PxqHTVzeG2c6eZd7974F9WvKUSdBeduUK3rjYyvBw is the address of the Mango v4 Program Governance
|
- 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
|
- Check out the release branch
|
||||||
|
|
||||||
git log program-v0.11.0..HEAD -- programs/mango-v4/
|
|
||||||
|
|
||||||
- Make sure the version is bumped in programs/mango-v4/Cargo.toml
|
- 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
|
- 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 group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
|
||||||
|
|
||||||
let oracles_and_vaults = group_context
|
let extra_accounts = group_context
|
||||||
.tokens
|
.tokens
|
||||||
.values()
|
.values()
|
||||||
.map(|value| value.oracle)
|
.map(|value| value.oracle)
|
||||||
.chain(group_context.perp_markets.values().map(|p| p.oracle))
|
.chain(group_context.perp_markets.values().map(|p| p.oracle))
|
||||||
.chain(group_context.tokens.values().flat_map(|value| value.vaults))
|
.chain(group_context.tokens.values().flat_map(|value| value.vaults))
|
||||||
|
.chain(group_context.address_lookup_tables.iter().copied())
|
||||||
.unique()
|
.unique()
|
||||||
.filter(|pk| *pk != Pubkey::default())
|
.filter(|pk| *pk != Pubkey::default())
|
||||||
.collect::<Vec<Pubkey>>();
|
.collect::<Vec<Pubkey>>();
|
||||||
|
@ -55,7 +56,7 @@ pub async fn save_snapshot(
|
||||||
serum_programs,
|
serum_programs,
|
||||||
open_orders_authority: mango_group,
|
open_orders_authority: mango_group,
|
||||||
},
|
},
|
||||||
oracles_and_vaults.clone(),
|
extra_accounts.clone(),
|
||||||
account_update_sender.clone(),
|
account_update_sender.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ pub async fn save_snapshot(
|
||||||
snapshot_interval: Duration::from_secs(6000),
|
snapshot_interval: Duration::from_secs(6000),
|
||||||
min_slot: first_websocket_slot + 10,
|
min_slot: first_websocket_slot + 10,
|
||||||
},
|
},
|
||||||
oracles_and_vaults,
|
extra_accounts,
|
||||||
account_update_sender,
|
account_update_sender,
|
||||||
);
|
);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
|
@ -49,3 +49,4 @@ tokio-stream = { version = "0.1.9"}
|
||||||
tokio-tungstenite = "0.16.1"
|
tokio-tungstenite = "0.16.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
|
hdrhistogram = "7.5.4"
|
|
@ -287,6 +287,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let mut metric_account_update_queue_len =
|
let mut metric_account_update_queue_len =
|
||||||
metrics.register_u64("account_update_queue_length".into());
|
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 metric_mango_accounts = metrics.register_u64("mango_accounts".into());
|
||||||
|
|
||||||
let mut mint_infos = HashMap::<TokenIndex, Pubkey>::new();
|
let mut mint_infos = HashMap::<TokenIndex, Pubkey>::new();
|
||||||
|
@ -299,6 +301,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.recv()
|
.recv()
|
||||||
.await
|
.await
|
||||||
.expect("channel not closed");
|
.expect("channel not closed");
|
||||||
|
let current_time = Instant::now();
|
||||||
metric_account_update_queue_len.set(account_update_receiver.len() as u64);
|
metric_account_update_queue_len.set(account_update_receiver.len() as u64);
|
||||||
|
|
||||||
message.update_chain_data(&mut chain_data.write().unwrap());
|
message.update_chain_data(&mut chain_data.write().unwrap());
|
||||||
|
@ -306,6 +309,15 @@ async fn main() -> anyhow::Result<()> {
|
||||||
match message {
|
match message {
|
||||||
Message::Account(account_write) => {
|
Message::Account(account_write) => {
|
||||||
let mut state = shared_state.write().unwrap();
|
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() {
|
if is_mango_account(&account_write.account, &mango_group).is_some() {
|
||||||
// e.g. to render debug logs RUST_LOG="liquidator=debug"
|
// e.g. to render debug logs RUST_LOG="liquidator=debug"
|
||||||
debug!(
|
debug!(
|
||||||
|
@ -320,8 +332,21 @@ async fn main() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Message::Snapshot(snapshot) => {
|
Message::Snapshot(snapshot) => {
|
||||||
let mut state = shared_state.write().unwrap();
|
let mut state = shared_state.write().unwrap();
|
||||||
|
let mut reception_time = None;
|
||||||
|
|
||||||
// Track all mango account pubkeys
|
// Track all mango account pubkeys
|
||||||
for update in snapshot.iter() {
|
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() {
|
if is_mango_account(&update.account, &mango_group).is_some() {
|
||||||
state.mango_accounts.insert(update.pubkey);
|
state.mango_accounts.insert(update.pubkey);
|
||||||
}
|
}
|
||||||
|
@ -335,6 +360,11 @@ async fn main() -> anyhow::Result<()> {
|
||||||
oracles.insert(perp_market.oracle);
|
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);
|
metric_mango_accounts.set(state.mango_accounts.len() as u64);
|
||||||
|
|
||||||
state.one_snapshot_done = true;
|
state.one_snapshot_done = true;
|
||||||
|
@ -374,35 +404,82 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let liquidation_job = tokio::spawn({
|
let liquidation_job = tokio::spawn({
|
||||||
let mut interval =
|
let mut interval =
|
||||||
mango_v4_client::delay_interval(Duration::from_millis(cli.check_interval_ms));
|
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();
|
let shared_state = shared_state.clone();
|
||||||
async move {
|
async move {
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
let account_addresses = {
|
let account_addresses = {
|
||||||
let state = shared_state.write().unwrap();
|
let mut state = shared_state.write().unwrap();
|
||||||
if !state.one_snapshot_done {
|
if !state.one_snapshot_done {
|
||||||
|
// discard first latency info as it will skew data too much
|
||||||
|
state.oldest_chain_event_reception_time = None;
|
||||||
continue;
|
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()
|
state.mango_accounts.iter().cloned().collect_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
liquidation.errors.update();
|
liquidation.errors.update();
|
||||||
liquidation.oracle_errors.update();
|
liquidation.oracle_errors.update();
|
||||||
|
|
||||||
|
if liquidation_start_time.is_none() {
|
||||||
|
liquidation_start_time = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
let liquidated = liquidation
|
let liquidated = liquidation
|
||||||
.maybe_liquidate_one(account_addresses.iter())
|
.maybe_liquidate_one(account_addresses.iter())
|
||||||
.await;
|
.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;
|
let mut took_tcs = false;
|
||||||
if !liquidated && cli.take_tcs == BoolArg::True {
|
if !liquidated && cli.take_tcs == BoolArg::True {
|
||||||
|
tcs_start_time = Some(tcs_start_time.unwrap_or(Instant::now()));
|
||||||
|
|
||||||
took_tcs = liquidation
|
took_tcs = liquidation
|
||||||
.maybe_take_token_conditional_swap(account_addresses.iter())
|
.maybe_take_token_conditional_swap(account_addresses.iter())
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
error!("error during maybe_take_token_conditional_swap: {err}");
|
error!("error during maybe_take_token_conditional_swap: {err}");
|
||||||
false
|
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 {
|
if liquidated || took_tcs {
|
||||||
|
@ -483,6 +560,9 @@ struct SharedState {
|
||||||
|
|
||||||
/// Is the first snapshot done? Only start checking account health when it is.
|
/// Is the first snapshot done? Only start checking account health when it is.
|
||||||
one_snapshot_done: bool,
|
one_snapshot_done: bool,
|
||||||
|
|
||||||
|
/// Oldest chain event not processed yet
|
||||||
|
oldest_chain_event_reception_time: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use hdrhistogram::Histogram;
|
||||||
|
use std::time::Duration;
|
||||||
use {
|
use {
|
||||||
std::collections::HashMap,
|
std::collections::HashMap,
|
||||||
std::sync::{atomic, Arc, Mutex, RwLock},
|
std::sync::{atomic, Arc, Mutex, RwLock},
|
||||||
|
@ -10,6 +12,7 @@ enum Value {
|
||||||
U64(Arc<atomic::AtomicU64>),
|
U64(Arc<atomic::AtomicU64>),
|
||||||
I64(Arc<atomic::AtomicI64>),
|
I64(Arc<atomic::AtomicI64>),
|
||||||
String(Arc<Mutex<String>>),
|
String(Arc<Mutex<String>>),
|
||||||
|
Latency(Arc<Mutex<Histogram<u64>>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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)]
|
#[derive(Clone)]
|
||||||
pub struct MetricI64 {
|
pub struct MetricI64 {
|
||||||
value: Arc<atomic::AtomicI64>,
|
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 {
|
pub fn register_string(&self, name: String) -> MetricString {
|
||||||
let mut registry = self.registry.write().unwrap();
|
let mut registry = self.registry.write().unwrap();
|
||||||
let value = registry
|
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_client::rpc_response::{Response, RpcKeyedAccount};
|
||||||
use solana_sdk::{account::AccountSharedData, pubkey::Pubkey};
|
use solana_sdk::{account::AccountSharedData, pubkey::Pubkey};
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
use std::{str::FromStr, sync::Arc};
|
use std::{str::FromStr, sync::Arc};
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ pub struct AccountUpdate {
|
||||||
pub pubkey: Pubkey,
|
pub pubkey: Pubkey,
|
||||||
pub slot: u64,
|
pub slot: u64,
|
||||||
pub account: AccountSharedData,
|
pub account: AccountSharedData,
|
||||||
|
pub reception_time: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountUpdate {
|
impl AccountUpdate {
|
||||||
|
@ -25,15 +27,22 @@ impl AccountUpdate {
|
||||||
pubkey,
|
pubkey,
|
||||||
slot: rpc.context.slot,
|
slot: rpc.context.slot,
|
||||||
account,
|
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)]
|
#[derive(Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Account(AccountUpdate),
|
Account(AccountUpdate),
|
||||||
Snapshot(Vec<AccountUpdate>),
|
Snapshot(Vec<AccountUpdate>),
|
||||||
Slot(Arc<solana_client::rpc_response::SlotUpdate>),
|
Slot(ChainSlotUpdate),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
|
@ -65,7 +74,7 @@ impl Message {
|
||||||
}
|
}
|
||||||
Message::Slot(slot_update) => {
|
Message::Slot(slot_update) => {
|
||||||
trace!("websocket slot message");
|
trace!("websocket slot message");
|
||||||
let slot_update = match **slot_update {
|
let slot_update = match *(slot_update.slot_update) {
|
||||||
solana_client::rpc_response::SlotUpdate::CreatedBank {
|
solana_client::rpc_response::SlotUpdate::CreatedBank {
|
||||||
slot, parent, ..
|
slot, parent, ..
|
||||||
} => Some(SlotData {
|
} => Some(SlotData {
|
||||||
|
|
|
@ -7,9 +7,9 @@ use anchor_lang::__private::bytemuck;
|
||||||
use mango_v4::{
|
use mango_v4::{
|
||||||
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
|
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
|
||||||
state::{
|
state::{
|
||||||
determine_oracle_type, load_whirlpool_state, oracle_state_unchecked, Group,
|
determine_oracle_type, load_orca_pool_state, load_raydium_pool_state,
|
||||||
MangoAccountValue, OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType,
|
oracle_state_unchecked, Group, MangoAccountValue, OracleAccountInfos, OracleConfig,
|
||||||
PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
|
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 {
|
fn get_fallback_quote_key(acc_info: &impl KeyedAccountReader) -> Pubkey {
|
||||||
let maybe_key = match determine_oracle_type(acc_info).ok() {
|
let maybe_key = match determine_oracle_type(acc_info).ok() {
|
||||||
Some(oracle_type) => match oracle_type {
|
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(),
|
Some(whirlpool) => whirlpool.get_quote_oracle().ok(),
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
|
OracleType::RaydiumCLMM => match load_raydium_pool_state(acc_info).ok() {
|
||||||
|
Some(pool) => pool.get_quote_oracle().ok(),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
_ => None,
|
_ => 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::AccountsDataClient;
|
||||||
use solana_rpc::rpc::rpc_accounts_scan::AccountsScanClient;
|
use solana_rpc::rpc::rpc_accounts_scan::AccountsScanClient;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
|
@ -56,6 +55,7 @@ impl AccountSnapshot {
|
||||||
.account
|
.account
|
||||||
.decode()
|
.decode()
|
||||||
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
|
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
|
||||||
|
reception_time: Instant::now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -75,6 +75,7 @@ impl AccountSnapshot {
|
||||||
account: ui_account
|
account: ui_account
|
||||||
.decode()
|
.decode()
|
||||||
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
|
.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 solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use tokio_stream::StreamMap;
|
use tokio_stream::StreamMap;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use crate::account_update_stream::{AccountUpdate, Message};
|
use crate::account_update_stream::{AccountUpdate, ChainSlotUpdate, Message};
|
||||||
use crate::AnyhowWrap;
|
use crate::AnyhowWrap;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -143,7 +143,10 @@ async fn feed_data(
|
||||||
},
|
},
|
||||||
message = slot_sub.next() => {
|
message = slot_sub.next() => {
|
||||||
if let Some(data) = message {
|
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 {
|
} else {
|
||||||
warn!("slot update stream closed");
|
warn!("slot update stream closed");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -200,7 +203,7 @@ pub async fn get_next_create_bank_slot(
|
||||||
match msg {
|
match msg {
|
||||||
Message::Slot(slot_update) => {
|
Message::Slot(slot_update) => {
|
||||||
if let solana_client::rpc_response::SlotUpdate::CreatedBank { slot, .. } =
|
if let solana_client::rpc_response::SlotUpdate::CreatedBank { slot, .. } =
|
||||||
*slot_update
|
*slot_update.slot_update
|
||||||
{
|
{
|
||||||
return Ok(slot);
|
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",
|
"name": "mango_v4",
|
||||||
"instructions": [
|
"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",
|
"name": "stubOracleCreate",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
|
@ -7871,13 +7931,8 @@
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "sequenceNumber",
|
||||||
"type": {
|
"type": "u8"
|
||||||
"array": [
|
|
||||||
"u8",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "netDeposits",
|
"name": "netDeposits",
|
||||||
|
@ -9669,13 +9724,8 @@
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "sequenceNumber",
|
||||||
"type": {
|
"type": "u8"
|
||||||
"array": [
|
|
||||||
"u8",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "netDeposits",
|
"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",
|
"name": "Serum3SelfTradeBehavior",
|
||||||
"docs": [
|
"docs": [
|
||||||
|
@ -11008,6 +11084,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenForceWithdraw"
|
"name": "TokenForceWithdraw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SequenceCheck"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HealthCheck"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11048,6 +11130,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "OrcaCLMM"
|
"name": "OrcaCLMM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RaydiumCLMM"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -14347,6 +14432,21 @@
|
||||||
"code": 6069,
|
"code": 6069,
|
||||||
"name": "TokenAssetLiquidationDisabled",
|
"name": "TokenAssetLiquidationDisabled",
|
||||||
"msg": "the asset does not allow liquidation"
|
"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]
|
[package]
|
||||||
name = "mango-v4"
|
name = "mango-v4"
|
||||||
version = "0.23.0"
|
version = "0.24.0"
|
||||||
description = "Created with Anchor"
|
description = "Created with Anchor"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
@ -75,3 +75,6 @@ rand = "0.8.4"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
num = "0.4.0"
|
num = "0.4.0"
|
||||||
regex = "1"
|
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_create::*;
|
||||||
pub use group_edit::*;
|
pub use group_edit::*;
|
||||||
pub use group_withdraw_insurance_fund::*;
|
pub use group_withdraw_insurance_fund::*;
|
||||||
|
pub use health_check::*;
|
||||||
pub use health_region::*;
|
pub use health_region::*;
|
||||||
pub use ix_gate_set::*;
|
pub use ix_gate_set::*;
|
||||||
pub use openbook_v2_cancel_order::*;
|
pub use openbook_v2_cancel_order::*;
|
||||||
|
@ -45,6 +46,7 @@ pub use perp_place_order::*;
|
||||||
pub use perp_settle_fees::*;
|
pub use perp_settle_fees::*;
|
||||||
pub use perp_settle_pnl::*;
|
pub use perp_settle_pnl::*;
|
||||||
pub use perp_update_funding::*;
|
pub use perp_update_funding::*;
|
||||||
|
pub use sequence_check::*;
|
||||||
pub use serum3_cancel_all_orders::*;
|
pub use serum3_cancel_all_orders::*;
|
||||||
pub use serum3_cancel_order::*;
|
pub use serum3_cancel_order::*;
|
||||||
pub use serum3_close_open_orders::*;
|
pub use serum3_close_open_orders::*;
|
||||||
|
@ -94,6 +96,7 @@ mod group_close;
|
||||||
mod group_create;
|
mod group_create;
|
||||||
mod group_edit;
|
mod group_edit;
|
||||||
mod group_withdraw_insurance_fund;
|
mod group_withdraw_insurance_fund;
|
||||||
|
mod health_check;
|
||||||
mod health_region;
|
mod health_region;
|
||||||
mod ix_gate_set;
|
mod ix_gate_set;
|
||||||
mod openbook_v2_cancel_order;
|
mod openbook_v2_cancel_order;
|
||||||
|
@ -123,6 +126,7 @@ mod perp_place_order;
|
||||||
mod perp_settle_fees;
|
mod perp_settle_fees;
|
||||||
mod perp_settle_pnl;
|
mod perp_settle_pnl;
|
||||||
mod perp_update_funding;
|
mod perp_update_funding;
|
||||||
|
mod sequence_check;
|
||||||
mod serum3_cancel_all_orders;
|
mod serum3_cancel_all_orders;
|
||||||
mod serum3_cancel_order;
|
mod serum3_cancel_order;
|
||||||
mod serum3_close_open_orders;
|
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];
|
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
|
/// Like AccountReader, but can also get the account pubkey
|
||||||
pub trait KeyedAccountReader: AccountReader {
|
pub trait KeyedAccountReader: AccountReader {
|
||||||
fn key(&self) -> &Pubkey;
|
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")]
|
#[cfg(feature = "solana-sdk")]
|
||||||
impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
|
impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
|
||||||
fn owner(&self) -> &Pubkey {
|
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")]
|
#[cfg(feature = "solana-sdk")]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct KeyedAccount {
|
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> {
|
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());
|
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());
|
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() {
|
if disc_bytes != &T::discriminator() {
|
||||||
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
|
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(bytemuck::from_bytes_mut(
|
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> {
|
fn load_mut_fully_unchecked<T: ZeroCopy + Owner>(&mut self) -> Result<&mut T> {
|
||||||
Ok(bytemuck::from_bytes_mut(
|
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,
|
MissingFeedForCLMMOracle,
|
||||||
#[msg("the asset does not allow liquidation")]
|
#[msg("the asset does not allow liquidation")]
|
||||||
TokenAssetLiquidationDisabled,
|
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 {
|
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
|
/// are passed because health needs to be computed for different baskets in
|
||||||
/// one instruction (such as for liquidation instructions).
|
/// one instruction (such as for liquidation instructions).
|
||||||
pub trait AccountRetriever {
|
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(
|
fn bank_and_oracle(
|
||||||
&self,
|
&self,
|
||||||
group: &Pubkey,
|
group: &Pubkey,
|
||||||
|
@ -45,11 +48,12 @@ pub trait AccountRetriever {
|
||||||
|
|
||||||
/// Assumes the account infos needed for the health computation follow a strict order.
|
/// 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
|
/// 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
|
/// 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
|
/// 6. fallback oracle accounts, order and existence of accounts is not guaranteed
|
||||||
pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
||||||
pub ais: Vec<T>,
|
pub ais: Vec<T>,
|
||||||
|
@ -63,20 +67,67 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
|
||||||
pub sol_oracle_index: Option<usize>,
|
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>(
|
pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||||
ais: &'a [AccountInfo<'info>],
|
ais: &'a [AccountInfo<'info>],
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
|
now_slot: u64,
|
||||||
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
|
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
|
||||||
let active_token_len = account.active_token_positions().count();
|
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_serum3_len = account.active_serum3_orders().count();
|
||||||
let active_perp_len = account.active_perp_positions().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_perp_len * 2 // PerpMarkets + Oracles
|
||||||
+ active_serum3_len; // open_orders
|
+ active_serum3_len; // open_orders
|
||||||
require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount,
|
require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount,
|
||||||
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
|
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
|
||||||
ais.len(), expected_ais,
|
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[..]
|
let usdc_oracle_index = ais[..]
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -87,11 +138,11 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||||
|
|
||||||
Ok(FixedOrderAccountRetriever {
|
Ok(FixedOrderAccountRetriever {
|
||||||
ais: AccountInfoRef::borrow_slice(ais)?,
|
ais: AccountInfoRef::borrow_slice(ais)?,
|
||||||
n_banks: active_token_len,
|
n_banks,
|
||||||
n_perps: active_perp_len,
|
n_perps: active_perp_len,
|
||||||
begin_perp: active_token_len * 2,
|
begin_perp: n_banks * 2,
|
||||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
begin_serum3: n_banks * 2 + active_perp_len * 2,
|
||||||
staleness_slot: Some(Clock::get()?.slot),
|
staleness_slot: Some(now_slot),
|
||||||
begin_fallback_oracles: expected_ais,
|
begin_fallback_oracles: expected_ais,
|
||||||
usdc_oracle_index,
|
usdc_oracle_index,
|
||||||
sol_oracle_index,
|
sol_oracle_index,
|
||||||
|
@ -99,11 +150,28 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
||||||
fn bank(&self, group: &Pubkey, account_index: usize, token_index: TokenIndex) -> Result<&Bank> {
|
fn bank(
|
||||||
let bank = self.ais[account_index].load::<Bank>()?;
|
&self,
|
||||||
require_keys_eq!(bank.group, *group);
|
group: &Pubkey,
|
||||||
require_eq!(bank.token_index, token_index);
|
active_token_position_index: usize,
|
||||||
Ok(bank)
|
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(
|
fn perp_market(
|
||||||
|
@ -146,25 +214,25 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: KeyedAccountReader> AccountRetriever for 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(
|
fn bank_and_oracle(
|
||||||
&self,
|
&self,
|
||||||
group: &Pubkey,
|
group: &Pubkey,
|
||||||
active_token_position_index: usize,
|
active_token_position_index: usize,
|
||||||
token_index: TokenIndex,
|
token_index: TokenIndex,
|
||||||
) -> Result<(&Bank, I80F48)> {
|
) -> Result<(&Bank, I80F48)> {
|
||||||
let bank_account_index = active_token_position_index;
|
let (bank_account_index, bank) =
|
||||||
let bank = self
|
self.bank(group, active_token_position_index, token_index)?;
|
||||||
.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 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_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_result = bank.oracle_price(oracle_acc_infos, self.staleness_slot);
|
||||||
let oracle_price = oracle_price_result.with_context(|| {
|
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> {
|
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(
|
fn bank_and_oracle(
|
||||||
&self,
|
&self,
|
||||||
_group: &Pubkey,
|
_group: &Pubkey,
|
||||||
|
@ -530,6 +602,8 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::state::{MangoAccount, MangoAccountValue};
|
||||||
|
|
||||||
use super::super::test::*;
|
use super::super::test::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use serum_dex::state::OpenOrders;
|
use serum_dex::state::OpenOrders;
|
||||||
|
@ -650,4 +724,98 @@ mod tests {
|
||||||
.perp_market_and_oracle_price(&group, 1, 5)
|
.perp_market_and_oracle_price(&group, 1, 5)
|
||||||
.is_err());
|
.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],
|
ais: &[AccountInfo],
|
||||||
now_ts: u64,
|
now_ts: u64,
|
||||||
) -> Result<I80F48> {
|
) -> 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))
|
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> {
|
pub fn perp_info(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpInfo> {
|
||||||
Ok(&self.perp_infos[self.perp_info_index(perp_market_index)?])
|
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
|
/// 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
|
/// 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.
|
/// 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,
|
account: &MangoAccountRef,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
now_ts: u64,
|
now_ts: u64,
|
||||||
|
@ -1246,22 +1252,49 @@ pub fn new_health_cache_skipping_bad_oracles(
|
||||||
new_health_cache_impl(account, retriever, now_ts, true)
|
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(
|
fn new_health_cache_impl(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
now_ts: u64,
|
now_ts: u64,
|
||||||
// If an oracle is stale or inconfident and the health contribution would
|
allow_skipping_banks: bool,
|
||||||
// not be negative, skip it. This decreases health, but maybe overall it's
|
|
||||||
// still positive?
|
|
||||||
skip_bad_oracles: bool,
|
|
||||||
) -> Result<HealthCache> {
|
) -> Result<HealthCache> {
|
||||||
// token contribution from token accounts
|
// token contribution from token accounts
|
||||||
let mut token_infos = Vec::with_capacity(account.active_token_positions().count());
|
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() {
|
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 =
|
let bank_oracle_result =
|
||||||
retriever.bank_and_oracle(&account.fixed.group, i, position.token_index);
|
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()
|
&& bank_oracle_result.is_oracle_error()
|
||||||
&& position.indexed_position >= 0
|
&& position.indexed_position >= 0
|
||||||
{
|
{
|
||||||
|
@ -1301,9 +1334,25 @@ fn new_health_cache_impl(
|
||||||
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
|
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
|
||||||
|
|
||||||
// find the TokenInfos for the market's base and quote tokens
|
// 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)?;
|
// and potentially skip the whole serum contribution if they are not available
|
||||||
let quote_info_index =
|
let info_index_results = (
|
||||||
find_token_info_index(&token_infos, serum_account.quote_token_index)?;
|
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
|
// add the amounts that are freely settleable immediately to token balances
|
||||||
let base_free = I80F48::from(oo.native_coin_free);
|
let base_free = I80F48::from(oo.native_coin_free);
|
||||||
|
@ -1329,6 +1378,12 @@ fn new_health_cache_impl(
|
||||||
i,
|
i,
|
||||||
perp_position.market_index,
|
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_infos.push(PerpInfo::new(
|
||||||
perp_position,
|
perp_position,
|
||||||
perp_market,
|
perp_market,
|
||||||
|
@ -1879,4 +1934,170 @@ mod tests {
|
||||||
test_health1_runner(testcase);
|
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::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::group_seeds;
|
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::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
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
|
// all vaults must have had matching banks
|
||||||
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
|
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
|
||||||
require_msg!(
|
require_msg_typed!(
|
||||||
has_bank,
|
has_bank,
|
||||||
|
MangoError::InvalidBank,
|
||||||
"missing bank for vault index {}, address {}",
|
"missing bank for vault index {}, address {}",
|
||||||
i,
|
i,
|
||||||
vaults[i].key
|
vaults[i].key
|
||||||
|
@ -387,12 +389,26 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check health before balance adjustments
|
// Check health before balance adjustments
|
||||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
// The vault-to-bank matching above ensures that the banks for the affected tokens are available.
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
let (now_ts, now_slot) = clock_now();
|
||||||
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,
|
||||||
|
)?;
|
||||||
|
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
|
|
||||||
// Prices for logging and net borrow checks
|
// 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![];
|
let mut oracle_prices = vec![];
|
||||||
for change in &changes {
|
for change in &changes {
|
||||||
let (_, oracle_price) = retriever.bank_and_oracle(
|
let (_, oracle_price) = retriever.bank_and_oracle(
|
||||||
|
@ -400,6 +416,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
change.bank_index,
|
change.bank_index,
|
||||||
change.token_index,
|
change.token_index,
|
||||||
)?;
|
)?;
|
||||||
|
// Sanity check
|
||||||
|
health_cache.token_info_index(change.token_index)?;
|
||||||
|
|
||||||
oracle_prices.push(oracle_price);
|
oracle_prices.push(oracle_price);
|
||||||
}
|
}
|
||||||
|
@ -502,8 +520,16 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check health after account position changes
|
// Check health after account position changes
|
||||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
|
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)?;
|
account.check_health_post(&health_cache, pre_init_health)?;
|
||||||
|
|
||||||
// Deactivate inactive token accounts after health check
|
// 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::Serum3PlaceOrderV2);
|
||||||
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
|
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;
|
group.ix_gate = ix_gate;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub use group_close::*;
|
||||||
pub use group_create::*;
|
pub use group_create::*;
|
||||||
pub use group_edit::*;
|
pub use group_edit::*;
|
||||||
pub use group_withdraw_insurance_fund::*;
|
pub use group_withdraw_insurance_fund::*;
|
||||||
|
pub use health_check::*;
|
||||||
pub use health_region::*;
|
pub use health_region::*;
|
||||||
pub use ix_gate_set::*;
|
pub use ix_gate_set::*;
|
||||||
pub use perp_cancel_all_orders::*;
|
pub use perp_cancel_all_orders::*;
|
||||||
|
@ -35,6 +36,7 @@ pub use perp_place_order::*;
|
||||||
pub use perp_settle_fees::*;
|
pub use perp_settle_fees::*;
|
||||||
pub use perp_settle_pnl::*;
|
pub use perp_settle_pnl::*;
|
||||||
pub use perp_update_funding::*;
|
pub use perp_update_funding::*;
|
||||||
|
pub use sequence_check::*;
|
||||||
pub use serum3_cancel_all_orders::*;
|
pub use serum3_cancel_all_orders::*;
|
||||||
pub use serum3_cancel_order::*;
|
pub use serum3_cancel_order::*;
|
||||||
pub use serum3_cancel_order_by_client_order_id::*;
|
pub use serum3_cancel_order_by_client_order_id::*;
|
||||||
|
@ -85,6 +87,7 @@ mod group_close;
|
||||||
mod group_create;
|
mod group_create;
|
||||||
mod group_edit;
|
mod group_edit;
|
||||||
mod group_withdraw_insurance_fund;
|
mod group_withdraw_insurance_fund;
|
||||||
|
mod health_check;
|
||||||
mod health_region;
|
mod health_region;
|
||||||
mod ix_gate_set;
|
mod ix_gate_set;
|
||||||
mod perp_cancel_all_orders;
|
mod perp_cancel_all_orders;
|
||||||
|
@ -104,6 +107,7 @@ mod perp_place_order;
|
||||||
mod perp_settle_fees;
|
mod perp_settle_fees;
|
||||||
mod perp_settle_pnl;
|
mod perp_settle_pnl;
|
||||||
mod perp_update_funding;
|
mod perp_update_funding;
|
||||||
|
mod sequence_check;
|
||||||
mod serum3_cancel_all_orders;
|
mod serum3_cancel_all_orders;
|
||||||
mod serum3_cancel_order;
|
mod serum3_cancel_order;
|
||||||
mod serum3_cancel_order_by_client_order_id;
|
mod serum3_cancel_order_by_client_order_id;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::accounts_ix::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
|
|
||||||
pub fn perp_liq_force_cancel_orders(
|
pub fn perp_liq_force_cancel_orders(
|
||||||
ctx: Context<PerpLiqForceCancelOrders>,
|
ctx: Context<PerpLiqForceCancelOrders>,
|
||||||
|
@ -11,10 +12,10 @@ pub fn perp_liq_force_cancel_orders(
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
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 mut health_cache = {
|
||||||
let retriever =
|
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")?
|
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_ix::*;
|
||||||
use crate::accounts_zerocopy::*;
|
use crate::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::{new_fixed_order_account_retriever, new_health_cache};
|
use crate::health::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[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_base_lots, 0);
|
||||||
require_gte!(order.max_quote_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;
|
let oracle_price;
|
||||||
|
|
||||||
// Update funding if possible.
|
// Update funding if possible.
|
||||||
|
@ -66,10 +67,21 @@ pub fn perp_place_order(
|
||||||
// Pre-health computation, _after_ perp position is created
|
// Pre-health computation, _after_ perp position is created
|
||||||
//
|
//
|
||||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let retriever =
|
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
ctx.remaining_accounts,
|
||||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
&account.borrow(),
|
||||||
.context("pre-withdraw init health")?;
|
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)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
Some((health_cache, pre_init_health))
|
Some((health_cache, pre_init_health))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog, TokenBalanceLog};
|
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<()> {
|
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
|
||||||
// max_settle_amount must greater than zero
|
// 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);
|
drop(perp_market);
|
||||||
|
|
||||||
// Verify that the result of settling did not violate the health of the account that lost money
|
// 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, now_slot) = clock_now();
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
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)?;
|
let health = compute_health(&account.borrow(), HealthType::Init, &retriever, now_ts)?;
|
||||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
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::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
|
||||||
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
|
|
||||||
pub fn serum3_liq_force_cancel_orders(
|
pub fn serum3_liq_force_cancel_orders(
|
||||||
ctx: Context<Serum3LiqForceCancelOrders>,
|
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
|
// Early return if if liquidation is not allowed or if market is not in force close
|
||||||
//
|
//
|
||||||
let mut health_cache = {
|
let mut health_cache = {
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
||||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
.context("create health cache")?;
|
.context("create health cache")?;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
|
||||||
use crate::serum3_cpi::{
|
use crate::serum3_cpi::{
|
||||||
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
|
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
|
||||||
};
|
};
|
||||||
|
use crate::util::clock_now;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
@ -40,6 +41,7 @@ pub fn serum3_place_order(
|
||||||
// Validation
|
// Validation
|
||||||
//
|
//
|
||||||
let receiver_token_index;
|
let receiver_token_index;
|
||||||
|
let payer_token_index;
|
||||||
{
|
{
|
||||||
let account = ctx.accounts.account.load_full()?;
|
let account = ctx.accounts.account.load_full()?;
|
||||||
// account constraint #1
|
// account constraint #1
|
||||||
|
@ -60,7 +62,7 @@ pub fn serum3_place_order(
|
||||||
// Validate bank and vault #3
|
// Validate bank and vault #3
|
||||||
let payer_bank = ctx.accounts.payer_bank.load()?;
|
let payer_bank = ctx.accounts.payer_bank.load()?;
|
||||||
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
|
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::Bid => serum_market.quote_token_index,
|
||||||
Serum3Side::Ask => serum_market.base_token_index,
|
Serum3Side::Ask => serum_market.base_token_index,
|
||||||
};
|
};
|
||||||
|
@ -76,10 +78,23 @@ pub fn serum3_place_order(
|
||||||
// Pre-health computation
|
// Pre-health computation
|
||||||
//
|
//
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
let (now_ts, now_slot) = clock_now();
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||||
let mut health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
ctx.remaining_accounts,
|
||||||
.context("pre-withdraw init health")?;
|
&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_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
Some(pre_init_health)
|
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
|
// 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
|
// the total serum3 potential amount assumes all reserved amounts convert at the current
|
||||||
// oracle price.
|
// 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 {
|
if receiver_bank_reduce_only {
|
||||||
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
|
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
|
||||||
let potential =
|
let potential =
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::accounts_zerocopy::*;
|
use crate::accounts_zerocopy::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use fixed::types::I80F48;
|
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<()> {
|
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
|
||||||
let group = ctx.accounts.group.load()?;
|
let group = ctx.accounts.group.load()?;
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
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 {
|
if group.collateral_fee_interval == 0 {
|
||||||
// By resetting, a new enabling of collateral fees will not immediately create a charge
|
// 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 health_cache = {
|
||||||
let retriever =
|
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)?
|
new_health_cache(&account.borrow(), &retriever, now_ts)?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::*;
|
use crate::logs::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
|
|
||||||
struct DepositCommon<'a, 'info> {
|
struct DepositCommon<'a, 'info> {
|
||||||
pub group: &'a AccountLoader<'info, Group>,
|
pub group: &'a AccountLoader<'info, Group>,
|
||||||
|
@ -119,13 +120,21 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
//
|
//
|
||||||
// Health computation
|
// Health computation
|
||||||
//
|
//
|
||||||
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
|
let (now_ts, now_slot) = clock_now();
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
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.
|
// 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.
|
// 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.
|
// 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.
|
// 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
|
// Group level deposit limit on account
|
||||||
let group = self.group.load()?;
|
let group = self.group.load()?;
|
||||||
if group.deposit_limit_quote > 0 {
|
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
|
let assets = cache
|
||||||
.health_assets_and_liabs_stable_assets(HealthType::Init)
|
.health_assets_and_liabs_stable_assets(HealthType::Init)
|
||||||
.0
|
.0
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
use crate::util::clock_now;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_spl::associated_token;
|
use anchor_spl::associated_token;
|
||||||
use anchor_spl::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 group = ctx.accounts.group.load()?;
|
||||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
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
|
// Create the account's position for that token index
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
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
|
// Health check _after_ the token position is guaranteed to exist
|
||||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let retriever =
|
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
ctx.remaining_accounts,
|
||||||
let hc_result = new_health_cache(&account.borrow(), &retriever, now_ts)
|
&account.borrow(),
|
||||||
.context("pre-withdraw health cache");
|
now_slot,
|
||||||
if hc_result.is_oracle_error() {
|
)?;
|
||||||
// We allow NOT checking the pre init health. That means later on the health
|
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
||||||
// check will be stricter (post_init > 0, without the post_init >= pre_init option)
|
&account.borrow(),
|
||||||
// Then later we can compute the health while ignoring potential nonnegative
|
&retriever,
|
||||||
// health contributions from tokens with stale oracles.
|
now_ts,
|
||||||
None
|
)
|
||||||
} else {
|
.context("pre-withdraw health cache")?;
|
||||||
let health_cache = hc_result?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
Some((health_cache, pre_init_health))
|
||||||
Some((health_cache, pre_init_health))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -156,26 +155,29 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
//
|
//
|
||||||
// Health check
|
// Health check
|
||||||
//
|
//
|
||||||
if !account.fixed.is_in_health_region() {
|
if let Some((mut health_cache, pre_init_health_lower_bound)) = pre_health_opt {
|
||||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
if health_cache.has_token_info(token_index) {
|
||||||
// This is the normal case
|
// 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)?;
|
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 {
|
} 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.
|
// We don't know the true pre_init_health: So require that our lower bound on
|
||||||
// If the health is good enough without those, we can pass.
|
// post health is strictly good enough.
|
||||||
//
|
account.check_health_post_checks_strict(post_init_health_lower_bound)?;
|
||||||
// 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)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -458,6 +458,22 @@ pub mod mango_v4 {
|
||||||
Ok(())
|
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:
|
// todo:
|
||||||
// ckamm: generally, using an I80F48 arg will make it harder to call
|
// ckamm: generally, using an I80F48 arg will make it harder to call
|
||||||
// because generic anchor clients won't know how to deal with it
|
// 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,
|
TokenConditionalSwapCreateLinearAuction = 70,
|
||||||
Serum3PlaceOrderV2 = 71,
|
Serum3PlaceOrderV2 = 71,
|
||||||
TokenForceWithdraw = 72,
|
TokenForceWithdraw = 72,
|
||||||
|
SequenceCheck = 73,
|
||||||
|
HealthCheck = 74,
|
||||||
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -123,8 +123,7 @@ pub struct MangoAccount {
|
||||||
|
|
||||||
pub bump: u8,
|
pub bump: u8,
|
||||||
|
|
||||||
#[derivative(Debug = "ignore")]
|
pub sequence_number: u8,
|
||||||
pub padding: [u8; 1],
|
|
||||||
|
|
||||||
// (Display only)
|
// (Display only)
|
||||||
// Cumulative (deposits - withdraws)
|
// Cumulative (deposits - withdraws)
|
||||||
|
@ -200,7 +199,7 @@ impl MangoAccount {
|
||||||
in_health_region: 0,
|
in_health_region: 0,
|
||||||
account_num: 0,
|
account_num: 0,
|
||||||
bump: 0,
|
bump: 0,
|
||||||
padding: Default::default(),
|
sequence_number: 0,
|
||||||
net_deposits: 0,
|
net_deposits: 0,
|
||||||
perp_spot_transfers: 0,
|
perp_spot_transfers: 0,
|
||||||
health_region_begin_init_health: 0,
|
health_region_begin_init_health: 0,
|
||||||
|
@ -325,7 +324,7 @@ pub struct MangoAccountFixed {
|
||||||
being_liquidated: u8,
|
being_liquidated: u8,
|
||||||
in_health_region: u8,
|
in_health_region: u8,
|
||||||
pub bump: u8,
|
pub bump: u8,
|
||||||
pub padding: [u8; 1],
|
pub sequence_number: u8,
|
||||||
pub net_deposits: i64,
|
pub net_deposits: i64,
|
||||||
pub perp_spot_transfers: i64,
|
pub perp_spot_transfers: i64,
|
||||||
pub health_region_begin_init_health: i64,
|
pub health_region_begin_init_health: i64,
|
||||||
|
@ -1458,6 +1457,13 @@ impl<
|
||||||
Ok(())
|
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> {
|
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
|
||||||
// Once maint_health falls below 0, we want to start liquidating,
|
// Once maint_health falls below 0, we want to start liquidating,
|
||||||
// we want to allow liquidation to continue until init_health is positive,
|
// we want to allow liquidation to continue until init_health is positive,
|
||||||
|
@ -2897,7 +2903,7 @@ mod tests {
|
||||||
being_liquidated: fixed.being_liquidated,
|
being_liquidated: fixed.being_liquidated,
|
||||||
in_health_region: fixed.in_health_region,
|
in_health_region: fixed.in_health_region,
|
||||||
bump: fixed.bump,
|
bump: fixed.bump,
|
||||||
padding: Default::default(),
|
sequence_number: 0,
|
||||||
net_deposits: fixed.net_deposits,
|
net_deposits: fixed.net_deposits,
|
||||||
perp_spot_transfers: fixed.perp_spot_transfers,
|
perp_spot_transfers: fixed.perp_spot_transfers,
|
||||||
health_region_begin_init_health: fixed.health_region_begin_init_health,
|
health_region_begin_init_health: fixed.health_region_begin_init_health,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub use amm_cpi::*;
|
||||||
pub use bank::*;
|
pub use bank::*;
|
||||||
pub use dynamic_account::*;
|
pub use dynamic_account::*;
|
||||||
pub use equity::*;
|
pub use equity::*;
|
||||||
|
@ -7,13 +8,13 @@ pub use mango_account_components::*;
|
||||||
pub use mint_info::*;
|
pub use mint_info::*;
|
||||||
pub use openbook_v2_market::*;
|
pub use openbook_v2_market::*;
|
||||||
pub use oracle::*;
|
pub use oracle::*;
|
||||||
pub use orca_cpi::*;
|
|
||||||
pub use orderbook::*;
|
pub use orderbook::*;
|
||||||
pub use perp_market::*;
|
pub use perp_market::*;
|
||||||
pub use serum3_market::*;
|
pub use serum3_market::*;
|
||||||
pub use stable_price::*;
|
pub use stable_price::*;
|
||||||
pub use token_conditional_swap::*;
|
pub use token_conditional_swap::*;
|
||||||
|
|
||||||
|
mod amm_cpi;
|
||||||
mod bank;
|
mod bank;
|
||||||
mod dynamic_account;
|
mod dynamic_account;
|
||||||
mod equity;
|
mod equity;
|
||||||
|
@ -23,7 +24,6 @@ mod mango_account_components;
|
||||||
mod mint_info;
|
mod mint_info;
|
||||||
mod openbook_v2_market;
|
mod openbook_v2_market;
|
||||||
mod oracle;
|
mod oracle;
|
||||||
mod orca_cpi;
|
|
||||||
mod orderbook;
|
mod orderbook;
|
||||||
mod perp_market;
|
mod perp_market;
|
||||||
mod serum3_market;
|
mod serum3_market;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::mem::size_of;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_lang::{AnchorDeserialize, Discriminator};
|
use anchor_lang::{AnchorDeserialize, Discriminator};
|
||||||
use derivative::Derivative;
|
use derivative::Derivative;
|
||||||
use fixed::types::{I80F48, U64F64};
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
use static_assertions::const_assert_eq;
|
use static_assertions::const_assert_eq;
|
||||||
use switchboard_program::FastRoundResultAccountData;
|
use switchboard_program::FastRoundResultAccountData;
|
||||||
|
@ -12,9 +12,9 @@ use switchboard_v2::AggregatorAccountData;
|
||||||
use crate::accounts_zerocopy::*;
|
use crate::accounts_zerocopy::*;
|
||||||
|
|
||||||
use crate::error::*;
|
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_CONSTANT_ZERO_INDEX: i8 = 12;
|
||||||
const DECIMAL_CONSTANTS: [I80F48; 25] = [
|
const DECIMAL_CONSTANTS: [I80F48; 25] = [
|
||||||
|
@ -117,6 +117,7 @@ pub enum OracleType {
|
||||||
SwitchboardV1,
|
SwitchboardV1,
|
||||||
SwitchboardV2,
|
SwitchboardV2,
|
||||||
OrcaCLMM,
|
OrcaCLMM,
|
||||||
|
RaydiumCLMM,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OracleState {
|
pub struct OracleState {
|
||||||
|
@ -195,6 +196,8 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|
||||||
return Ok(OracleType::SwitchboardV1);
|
return Ok(OracleType::SwitchboardV1);
|
||||||
} else if acc_info.owner() == &orca_mainnet_whirlpool::ID {
|
} else if acc_info.owner() == &orca_mainnet_whirlpool::ID {
|
||||||
return Ok(OracleType::OrcaCLMM);
|
return Ok(OracleType::OrcaCLMM);
|
||||||
|
} else if acc_info.owner() == &raydium_mainnet::ID {
|
||||||
|
return Ok(OracleType::RaydiumCLMM);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(MangoError::UnknownOracleType.into())
|
Err(MangoError::UnknownOracleType.into())
|
||||||
|
@ -205,18 +208,19 @@ pub fn check_is_valid_fallback_oracle(acc_info: &impl KeyedAccountReader) -> Res
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let oracle_type = determine_oracle_type(acc_info)?;
|
let oracle_type = determine_oracle_type(acc_info)?;
|
||||||
if oracle_type == OracleType::OrcaCLMM {
|
let valid_oracle = match oracle_type {
|
||||||
let whirlpool = load_whirlpool_state(acc_info)?;
|
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
|
require!(valid_oracle, MangoError::UnexpectedOracle);
|
||||||
|| 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +257,7 @@ fn pyth_get_price(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pyth_state(
|
pub fn get_pyth_state(
|
||||||
acc_info: &(impl KeyedAccountReader + ?Sized),
|
acc_info: &(impl KeyedAccountReader + ?Sized),
|
||||||
base_decimals: u8,
|
base_decimals: u8,
|
||||||
) -> Result<OracleState> {
|
) -> Result<OracleState> {
|
||||||
|
@ -404,56 +408,32 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OracleType::OrcaCLMM => {
|
OracleType::OrcaCLMM => {
|
||||||
let whirlpool = load_whirlpool_state(oracle_info)?;
|
let whirlpool = load_orca_pool_state(oracle_info)?;
|
||||||
|
let clmm_price = whirlpool.get_clmm_price();
|
||||||
let inverted = whirlpool.is_inverted();
|
let quote_oracle_state = whirlpool.quote_state_unchecked(acc_infos)?;
|
||||||
let quote_state = if inverted {
|
let price = clmm_price * quote_oracle_state.price;
|
||||||
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;
|
|
||||||
OracleState {
|
OracleState {
|
||||||
price,
|
price,
|
||||||
last_update_slot: quote_state.last_update_slot,
|
last_update_slot: quote_oracle_state.last_update_slot,
|
||||||
deviation: quote_state.deviation,
|
deviation: quote_oracle_state.deviation,
|
||||||
oracle_type: OracleType::OrcaCLMM,
|
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(
|
pub fn oracle_log_context(
|
||||||
name: &str,
|
name: &str,
|
||||||
state: &OracleState,
|
state: &OracleState,
|
||||||
|
@ -545,7 +525,87 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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
|
// add ability to find fixtures
|
||||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
d.push("resources/test");
|
d.push("resources/test");
|
||||||
|
@ -558,67 +618,13 @@ mod tests {
|
||||||
9, // SOL/USDC pool
|
9, // SOL/USDC pool
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
|
||||||
OracleType::Pyth,
|
OracleType::RaydiumCLMM,
|
||||||
Pubkey::default(),
|
raydium_mainnet::ID,
|
||||||
6,
|
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 {
|
for fixture in fixtures {
|
||||||
let filename = format!("resources/test/{}.bin", fixture.0);
|
let filename = format!("resources/test/{}.bin", fixture.0);
|
||||||
let mut clmm_data = read_file(find_file(&filename).unwrap());
|
let mut clmm_data = read_file(find_file(&filename).unwrap());
|
||||||
|
@ -642,4 +648,47 @@ mod tests {
|
||||||
|
|
||||||
Ok(())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -10,7 +10,8 @@ pub use program_test::*;
|
||||||
|
|
||||||
pub use super::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_alt;
|
||||||
mod test_bankrupt_tokens;
|
mod test_bankrupt_tokens;
|
||||||
|
@ -21,6 +22,7 @@ mod test_collateral_fees;
|
||||||
mod test_delegate;
|
mod test_delegate;
|
||||||
mod test_fees_buyback_with_mngo;
|
mod test_fees_buyback_with_mngo;
|
||||||
mod test_force_close;
|
mod test_force_close;
|
||||||
|
mod test_health_check;
|
||||||
mod test_health_compute;
|
mod test_health_compute;
|
||||||
mod test_health_region;
|
mod test_health_region;
|
||||||
mod test_ix_gate_set;
|
mod test_ix_gate_set;
|
||||||
|
@ -35,6 +37,7 @@ mod test_perp_settle;
|
||||||
mod test_perp_settle_fees;
|
mod test_perp_settle_fees;
|
||||||
mod test_position_lifetime;
|
mod test_position_lifetime;
|
||||||
mod test_reduce_only;
|
mod test_reduce_only;
|
||||||
|
mod test_replay;
|
||||||
mod test_serum;
|
mod test_serum;
|
||||||
mod test_stale_oracles;
|
mod test_stale_oracles;
|
||||||
mod test_token_conditional_swap;
|
mod test_token_conditional_swap;
|
||||||
|
|
|
@ -450,7 +450,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let maint_health = account_maint_health(solana, account).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;
|
let start_time = solana.clock_timestamp().await;
|
||||||
|
|
||||||
|
@ -476,17 +476,17 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let maint_health = account_maint_health(solana, account).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);
|
||||||
|
|
||||||
solana.set_clock_timestamp(start_time + 1500).await;
|
solana.set_clock_timestamp(start_time + 1500).await;
|
||||||
|
|
||||||
let maint_health = account_maint_health(solana, account).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;
|
solana.set_clock_timestamp(start_time + 3000).await;
|
||||||
|
|
||||||
let maint_health = account_maint_health(solana, account).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;
|
solana.set_clock_timestamp(start_time + 1600).await;
|
||||||
|
|
||||||
|
@ -507,11 +507,11 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let maint_health = account_maint_health(solana, account).await;
|
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;
|
let bank: Bank = solana.get_account(tokens[0].bank).await;
|
||||||
assert!(assert_equal_fixed_f64(bank.maint_asset_weight, 0.7, 1e-4));
|
assert_eq_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_liab_weight, 1.3, 1e-4);
|
||||||
assert_eq!(bank.maint_weight_shift_duration_inv, I80F48::ZERO);
|
assert_eq!(bank.maint_weight_shift_duration_inv, I80F48::ZERO);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -687,3 +687,365 @@ async fn test_bank_deposit_limit() -> Result<(), TransportError> {
|
||||||
|
|
||||||
Ok(())
|
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
|
// 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!)
|
// (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,
|
solana,
|
||||||
TokenWithdrawInstruction {
|
TokenWithdrawInstruction {
|
||||||
amount: 4000,
|
amount: 4000,
|
||||||
|
@ -231,12 +231,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
||||||
token_account: payer_mint_accounts[0],
|
token_account: payer_mint_accounts[0],
|
||||||
bank_index: 0,
|
bank_index: 0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::BankNetBorrowsLimitReached
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::BankNetBorrowsLimitReached.into(),
|
|
||||||
"".into(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// succeeds because is not a borrow
|
// 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;
|
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 10.0).await;
|
||||||
|
|
||||||
// cannot borrow anything: net borrowed 1002 * price 10.0 > limit 6000
|
// cannot borrow anything: net borrowed 1002 * price 10.0 > limit 6000
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenWithdrawInstruction {
|
TokenWithdrawInstruction {
|
||||||
amount: 1,
|
amount: 1,
|
||||||
|
@ -324,12 +319,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
||||||
token_account: payer_mint_accounts[0],
|
token_account: payer_mint_accounts[0],
|
||||||
bank_index: 0,
|
bank_index: 0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::BankNetBorrowsLimitReached
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::BankNetBorrowsLimitReached.into(),
|
|
||||||
"".into(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// can still withdraw
|
// 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;
|
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
|
// cannot borrow this much: (net borrowed 1000 + new borrow 201) * price 5.0 > limit 6000
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenWithdrawInstruction {
|
TokenWithdrawInstruction {
|
||||||
amount: 200,
|
amount: 200,
|
||||||
|
@ -360,12 +350,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
|
||||||
token_account: payer_mint_accounts[0],
|
token_account: payer_mint_accounts[0],
|
||||||
bank_index: 0,
|
bank_index: 0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::BankNetBorrowsLimitReached
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::BankNetBorrowsLimitReached.into(),
|
|
||||||
"".into(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// can borrow smaller amounts: (net borrowed 1000 + new borrow 199) * price 5.0 < limit 6000
|
// 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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
last_time = solana.clock_timestamp().await;
|
last_time = solana.clock_timestamp().await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(
|
||||||
account_position_f64(solana, account, tokens[0].bank).await,
|
account_position_f64(solana, account, tokens[0].bank).await,
|
||||||
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
|
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
|
||||||
0.01
|
0.01
|
||||||
));
|
);
|
||||||
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
|
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -208,11 +208,11 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
//last_time = solana.clock_timestamp().await;
|
//last_time = solana.clock_timestamp().await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(
|
||||||
account_position_f64(solana, account, tokens[0].bank).await,
|
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))),
|
last_balance * (1.0 - 0.1 * (7.0 / 24.0) * (720.0 / (last_balance * 0.8))),
|
||||||
0.01
|
0.01
|
||||||
));
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,16 +185,16 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> {
|
||||||
assert_eq!(before_fees_accrued - after_fees_accrued, 19);
|
assert_eq!(before_fees_accrued - after_fees_accrued, 19);
|
||||||
|
|
||||||
// token[1] swapped at discount for token[0]
|
// token[1] swapped at discount for token[0]
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
fees_token_position_after - fees_token_position_before,
|
fees_token_position_after - fees_token_position_before,
|
||||||
19.0 / 2.0,
|
19.0 / 2.0,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
mngo_token_position_after - mngo_token_position_before,
|
mngo_token_position_after - mngo_token_position_before,
|
||||||
-19.0 / 3.0 / 1.2,
|
-19.0 / 3.0 / 1.2,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -357,18 +357,18 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
|
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(),
|
mango_account_0.perps[0].quote_position_native(),
|
||||||
-99.99,
|
-99.99,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
|
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(),
|
mango_account_1.perps[0].quote_position_native(),
|
||||||
99.98,
|
99.98,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
// Market needs to be in force close
|
// Market needs to be in force close
|
||||||
assert!(send_tx(
|
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;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
|
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(),
|
mango_account_0.perps[0].quote_position_native(),
|
||||||
0.009,
|
0.009,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
|
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(),
|
mango_account_1.perps[0].quote_position_native(),
|
||||||
-0.0199,
|
-0.0199,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
Ok(())
|
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]
|
#[tokio::test]
|
||||||
async fn test_health_compute_serum() -> Result<(), TransportError> {
|
async fn test_health_compute_serum() -> Result<(), TransportError> {
|
||||||
let mut test_builder = TestContextBuilder::new();
|
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 context = test_builder.start_default().await;
|
||||||
let solana = &context.solana.clone();
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
|
|
@ -337,11 +337,11 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
|
||||||
liqor_quote_before + 12
|
liqor_quote_before + 12
|
||||||
);
|
);
|
||||||
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
acc_data.perps[0].quote_position_native(),
|
acc_data.perps[0].quote_position_native(),
|
||||||
-50.0 + 11.0 + 27.0,
|
-50.0 + 11.0 + 27.0,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert_eq!(acc_data.being_liquidated, 0);
|
assert_eq!(acc_data.being_liquidated, 0);
|
||||||
let (_liqor_data, liqor_perp) = liqor_info(perp_market, liqor).await;
|
let (_liqor_data, liqor_perp) = liqor_info(perp_market, liqor).await;
|
||||||
assert_eq!(liqor_perp.quote_position_native(), -11);
|
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 liqee_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
|
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_data.perps[0].quote_position_native(),
|
||||||
-liqor_amount,
|
-liqor_amount,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
-20.0 * 100.0 + liqee_amount,
|
-20.0 * 100.0 + liqee_amount,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].realized_pnl_for_position_native,
|
liqee_data.perps[0].realized_pnl_for_position_native,
|
||||||
liqee_amount - 1000.0,
|
liqee_amount - 1000.0,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
// stable price is 1.0, so 0.2 * 1000
|
// stable price is 1.0, so 0.2 * 1000
|
||||||
assert_eq!(liqee_data.perps[0].recurring_settle_pnl_allowance, 201);
|
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,
|
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
|
||||||
liqor_amount - liqee_amount,
|
liqor_amount - liqee_amount,
|
||||||
0.1,
|
0.1,
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
|
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
|
||||||
liqor_amount - liqee_amount,
|
liqor_amount - liqee_amount,
|
||||||
0.1,
|
0.1,
|
||||||
));
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Liquidate base position max
|
// 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 liqee_amount_2 = 6.0 * 100.0 * 0.6 * (1.0 - 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 6);
|
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_data.perps[0].quote_position_native(),
|
||||||
-liqor_amount - liqor_amount_2,
|
-liqor_amount - liqor_amount_2,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
-20.0 * 100.0 + liqee_amount + liqee_amount_2,
|
-20.0 * 100.0 + liqee_amount + liqee_amount_2,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
// verify health is good again
|
// verify health is good again
|
||||||
send_tx(
|
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 liqee_amount_3 = 10.0 * 100.0 * 1.32 * (1.0 + 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 16 - 10);
|
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_data.perps[0].quote_position_native(),
|
||||||
-liqor_amount - liqor_amount_2 + liqor_amount_3,
|
-liqor_amount - liqor_amount_2 + liqor_amount_3,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), -10);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), -10);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
20.0 * 100.0 - liqee_amount_3,
|
20.0 * 100.0 - liqee_amount_3,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
|
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
|
||||||
liqee_amount_3 - liqor_amount_3,
|
liqee_amount_3 - liqor_amount_3,
|
||||||
0.1,
|
0.1,
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
|
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
|
||||||
liqee_amount_3 - liqor_amount_3,
|
liqee_amount_3 - liqor_amount_3,
|
||||||
0.1,
|
0.1,
|
||||||
));
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Liquidate base position max
|
// 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 liqee_amount_4 = 7.0 * 100.0 * 1.32 * (1.0 + 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 6 - 7);
|
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_data.perps[0].quote_position_native(),
|
||||||
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4,
|
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), -3);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), -3);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4,
|
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
// verify health is good again
|
// verify health is good again
|
||||||
send_tx(
|
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 liqee_amount_5 = 3.0 * 100.0 * 2.0 * (1.0 + 0.05);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 3);
|
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_data.perps[0].quote_position_native(),
|
||||||
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4 + liqor_amount_5,
|
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4 + liqor_amount_5,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4 - liqee_amount_5,
|
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4 - liqee_amount_5,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Can settle-pnl even though health is negative
|
// 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);
|
assert!(remaining_pnl < 0.0);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
remaining_pnl,
|
remaining_pnl,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
account_position(solana, account_1, quote_token.bank).await,
|
account_position(solana, account_1, quote_token.bank).await,
|
||||||
liqee_quote_deposits_before as i64
|
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
|
// insurance fund was depleted and the liqor received it
|
||||||
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
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_data.tokens[0].native("e_bank),
|
||||||
liqor_before.tokens[0].native("e_bank).to_num::<f64>() + insurance_vault_funding as f64,
|
liqor_before.tokens[0].native("e_bank).to_num::<f64>() + insurance_vault_funding as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqor_data.tokens[1].native(&settle_bank),
|
liqor_data.tokens[1].native(&settle_bank),
|
||||||
liqor_before.tokens[1].native(&settle_bank).to_num::<f64>()
|
liqor_before.tokens[1].native(&settle_bank).to_num::<f64>()
|
||||||
- liqee_settle_limit_before as f64,
|
- liqee_settle_limit_before as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
// liqor took over the max possible negative pnl
|
// liqor took over the max possible negative pnl
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqor_data.perps[0].quote_position_native(),
|
liqor_data.perps[0].quote_position_native(),
|
||||||
liqor_before.perps[0]
|
liqor_before.perps[0]
|
||||||
.quote_position_native()
|
.quote_position_native()
|
||||||
.to_num::<f64>()
|
.to_num::<f64>()
|
||||||
- liq_perp_quote_amount,
|
- liq_perp_quote_amount,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
// liqee exited liquidation
|
// liqee exited liquidation
|
||||||
assert!(account_init_health(solana, account_1).await >= 0.0);
|
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();
|
.unwrap();
|
||||||
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
|
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();
|
let open_interest = 2 * liqor_data.perps[0].base_position_lots.abs();
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
perp_market.long_funding,
|
perp_market.long_funding,
|
||||||
socialized_amount / open_interest as f64,
|
socialized_amount / open_interest as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
perp_market.short_funding,
|
perp_market.short_funding,
|
||||||
-socialized_amount / open_interest as f64,
|
-socialized_amount / open_interest as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
account0_before.perps[0].unsettled_funding(&perp_market),
|
account0_before.perps[0].unsettled_funding(&perp_market),
|
||||||
socialized_amount / 2.0,
|
socialized_amount / 2.0,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,22 +297,22 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
assert_eq!(liqor_data.perps[0].base_position_lots(), 1);
|
assert_eq!(liqor_data.perps[0].base_position_lots(), 1);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqor_data.perps[0].quote_position_native(),
|
liqor_data.perps[0].quote_position_native(),
|
||||||
100.0 + 600.0 - 2100.0 * 0.95,
|
100.0 + 600.0 - 2100.0 * 0.95,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
account_position(solana, liqor, settle_token.bank).await,
|
account_position(solana, liqor, settle_token.bank).await,
|
||||||
10000 - 95 - 570
|
10000 - 95 - 570
|
||||||
);
|
);
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 9);
|
assert_eq!(liqee_data.perps[0].base_position_lots(), 9);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqee_data.perps[0].quote_position_native(),
|
||||||
-10000.0 - 100.0 - 600.0 + 2100.0 * 0.95,
|
-10000.0 - 100.0 - 600.0 + 2100.0 * 0.95,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
account_position(solana, account_0, settle_token.bank).await,
|
account_position(solana, account_0, settle_token.bank).await,
|
||||||
95 + 570
|
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,
|
// 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
|
// so the platform fee is
|
||||||
let platform_fee = 20.0 * (1.0 - 1.01 * 1.01 / (1.02 * 1.02));
|
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,
|
account_position_f64(solana, vault_account, collateral_token2.bank).await,
|
||||||
100000.0 + 20.0 - platform_fee,
|
100000.0 + 20.0 - platform_fee,
|
||||||
0.001,
|
0.001,
|
||||||
));
|
);
|
||||||
|
|
||||||
// Verify platform liq fee tracking
|
// Verify platform liq fee tracking
|
||||||
let colbank = solana.get_account::<Bank>(collateral_token2.bank).await;
|
let colbank = solana.get_account::<Bank>(collateral_token2.bank).await;
|
||||||
assert!(assert_equal_fixed_f64(
|
assert_eq_fixed_f64!(colbank.collected_fees_native, platform_fee, 0.001);
|
||||||
colbank.collected_fees_native,
|
assert_eq_fixed_f64!(colbank.collected_liquidation_fees, platform_fee, 0.001);
|
||||||
platform_fee,
|
|
||||||
0.001
|
|
||||||
));
|
|
||||||
assert!(assert_equal_fixed_f64(
|
|
||||||
colbank.collected_liquidation_fees,
|
|
||||||
platform_fee,
|
|
||||||
0.001
|
|
||||||
));
|
|
||||||
|
|
||||||
let liqee = get_mango_account(solana, account).await;
|
let liqee = get_mango_account(solana, account).await;
|
||||||
assert!(liqee.being_liquidated());
|
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 payer_mint0_account = context.users[1].token_accounts[0];
|
||||||
let loan_origination_fee = 0.0005;
|
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)
|
// 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
|
margin_account_initial + withdraw_amount - deposit_amount
|
||||||
);
|
);
|
||||||
// no fee because user had positive balance
|
// no fee because user had positive balance
|
||||||
assert!(balance_f64eq(
|
assert_eq_f64!(
|
||||||
account_position_f64(solana, account, bank).await,
|
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
|
// 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,
|
solana.token_account_balance(margin_account).await,
|
||||||
margin_account_initial - deposit_amount
|
margin_account_initial - deposit_amount
|
||||||
);
|
);
|
||||||
assert!(balance_f64eq(
|
assert_eq_f64!(
|
||||||
account_position_f64(solana, account, bank).await,
|
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
|
// 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,
|
solana.token_account_balance(margin_account).await,
|
||||||
margin_account_initial + withdraw_amount - deposit_amount
|
margin_account_initial + withdraw_amount - deposit_amount
|
||||||
);
|
);
|
||||||
assert!(balance_f64eq(
|
assert_eq_f64!(
|
||||||
account_position_f64(solana, account, bank).await,
|
account_position_f64(solana, account, bank).await,
|
||||||
(deposit_amount_initial + deposit_amount - withdraw_amount) as f64
|
(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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -255,9 +255,6 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
|
||||||
let owner_accounts = context.users[0].token_accounts.clone();
|
let owner_accounts = context.users[0].token_accounts.clone();
|
||||||
let payer_accounts = context.users[1].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)
|
// 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;
|
let mango_withdraw_amount = account_position_f64(solana, account, tokens[0].bank).await;
|
||||||
assert!(balance_f64eq(
|
assert_eq_f64!(
|
||||||
mango_withdraw_amount,
|
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;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -732,3 +730,112 @@ async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
|
||||||
|
|
||||||
Ok(())
|
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;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
|
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(),
|
mango_account_0.perps[0].quote_position_native(),
|
||||||
-99.99,
|
-99.99,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
|
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(),
|
mango_account_1.perps[0].quote_position_native(),
|
||||||
99.98,
|
99.98,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: closing perp positions
|
// 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;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
|
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(),
|
mango_account_0.perps[0].quote_position_native(),
|
||||||
0.02,
|
0.02,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
|
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(),
|
mango_account_1.perps[0].quote_position_native(),
|
||||||
-0.04,
|
-0.04,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
// settle pnl and fees to bring quote_position_native fully to 0
|
// settle pnl and fees to bring quote_position_native fully to 0
|
||||||
send_tx(
|
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;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 2);
|
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(),
|
mango_account_0.perps[0].quote_position_native(),
|
||||||
-19998.0,
|
-19998.0,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -2);
|
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(),
|
mango_account_1.perps[0].quote_position_native(),
|
||||||
19996.0,
|
19996.0,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Place a pegged order and check how it behaves with oracle changes
|
// 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 mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
let perp_0 = mango_account_0.perps[0];
|
let perp_0 = mango_account_0.perps[0];
|
||||||
assert_eq!(perp_0.base_position_lots(), 1);
|
assert_eq!(perp_0.base_position_lots(), 1);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
perp_0.quote_position_native(),
|
perp_0.quote_position_native(),
|
||||||
-200_000.0 + 150_000.0,
|
-200_000.0 + 150_000.0,
|
||||||
0.001
|
0.001
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(perp_0.realized_pnl_for_position_native, 50_000.0, 0.001);
|
||||||
perp_0.realized_pnl_for_position_native,
|
|
||||||
50_000.0,
|
|
||||||
0.001
|
|
||||||
));
|
|
||||||
|
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
let perp_1 = mango_account_1.perps[0];
|
let perp_1 = mango_account_1.perps[0];
|
||||||
assert_eq!(perp_1.base_position_lots(), -1);
|
assert_eq!(perp_1.base_position_lots(), -1);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(perp_1.quote_position_native(), 200_000.0 - 150_000.0, 0.001);
|
||||||
perp_1.quote_position_native(),
|
assert_eq_fixed_f64!(perp_1.realized_pnl_for_position_native, -50_000.0, 0.001);
|
||||||
200_000.0 - 150_000.0,
|
|
||||||
0.001
|
|
||||||
));
|
|
||||||
assert!(assert_equal(
|
|
||||||
perp_1.realized_pnl_for_position_native,
|
|
||||||
-50_000.0,
|
|
||||||
0.001
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1593,6 +1581,138 @@ async fn test_perp_cancel_with_in_flight_events() -> Result<(), TransportError>
|
||||||
Ok(())
|
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) {
|
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
|
||||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
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
|
// Cannot settle with yourself
|
||||||
let result = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
settler,
|
settler,
|
||||||
|
@ -185,17 +185,11 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
||||||
account_b: account_0,
|
account_b: account_0,
|
||||||
perp_market,
|
perp_market,
|
||||||
},
|
},
|
||||||
)
|
MangoError::CannotSettleWithSelf
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_mango_error(
|
|
||||||
&result,
|
|
||||||
MangoError::CannotSettleWithSelf.into(),
|
|
||||||
"Cannot settle with yourself".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cannot settle position that does not exist
|
// Cannot settle position that does not exist
|
||||||
let result = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
settler,
|
settler,
|
||||||
|
@ -204,13 +198,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
||||||
account_b: account_1,
|
account_b: account_1,
|
||||||
perp_market: perp_market_2,
|
perp_market: perp_market_2,
|
||||||
},
|
},
|
||||||
)
|
MangoError::PerpPositionDoesNotExist
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_mango_error(
|
|
||||||
&result,
|
|
||||||
MangoError::PerpPositionDoesNotExist.into(),
|
|
||||||
"Cannot settle a position that does not exist".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Test funding settlement
|
// 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;
|
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1200.0).await;
|
||||||
|
|
||||||
// Account a must be the profitable one
|
// Account a must be the profitable one
|
||||||
let result = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
settler,
|
settler,
|
||||||
|
@ -244,13 +232,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
|
||||||
account_b: account_0,
|
account_b: account_0,
|
||||||
perp_market,
|
perp_market,
|
||||||
},
|
},
|
||||||
)
|
MangoError::ProfitabilityMismatch
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_mango_error(
|
|
||||||
&result,
|
|
||||||
MangoError::ProfitabilityMismatch.into(),
|
|
||||||
"Account a must be the profitable one".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Change the oracle to a more reasonable price
|
// 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
|
// 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
|
// we are in the same window, and we settled max. possible in previous attempt
|
||||||
let result = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
PerpSettlePnlInstruction {
|
PerpSettlePnlInstruction {
|
||||||
settler,
|
settler,
|
||||||
|
@ -1047,12 +1029,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
|
||||||
account_b: account_1,
|
account_b: account_1,
|
||||||
perp_market,
|
perp_market,
|
||||||
},
|
},
|
||||||
)
|
MangoError::ProfitabilityMismatch
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&result,
|
|
||||||
MangoError::ProfitabilityMismatch.into(),
|
|
||||||
"Account A has no settleable positive pnl left".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -166,52 +166,40 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
|
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(),
|
mango_account_0.perps[0].quote_position_native(),
|
||||||
-100_020.0,
|
-100_020.0,
|
||||||
0.01
|
0.01
|
||||||
));
|
);
|
||||||
|
|
||||||
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
|
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(),
|
mango_account_1.perps[0].quote_position_native(),
|
||||||
100_000.0,
|
100_000.0,
|
||||||
0.01
|
0.01
|
||||||
));
|
);
|
||||||
|
|
||||||
// Cannot settle position that does not exist
|
// Cannot settle position that does not exist
|
||||||
let result = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
PerpSettleFeesInstruction {
|
PerpSettleFeesInstruction {
|
||||||
account: account_1,
|
account: account_1,
|
||||||
perp_market: perp_market_2,
|
perp_market: perp_market_2,
|
||||||
max_settle_amount: u64::MAX,
|
max_settle_amount: u64::MAX,
|
||||||
},
|
},
|
||||||
)
|
MangoError::PerpPositionDoesNotExist
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_mango_error(
|
|
||||||
&result,
|
|
||||||
MangoError::PerpPositionDoesNotExist.into(),
|
|
||||||
"Cannot settle a position that does not exist".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// max_settle_amount must be greater than zero
|
// max_settle_amount must be greater than zero
|
||||||
let result = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
PerpSettleFeesInstruction {
|
PerpSettleFeesInstruction {
|
||||||
account: account_1,
|
account: account_1,
|
||||||
perp_market: perp_market,
|
perp_market: perp_market,
|
||||||
max_settle_amount: 0,
|
max_settle_amount: 0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::MaxSettleAmountMustBeGreaterThanZero
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_mango_error(
|
|
||||||
&result,
|
|
||||||
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
|
|
||||||
"max_settle_amount must be greater than zero".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Test funding settlement
|
// TODO: Test funding settlement
|
||||||
|
@ -247,20 +235,20 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
|
||||||
// No change
|
// No change
|
||||||
{
|
{
|
||||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
mango_account_0.perps[0]
|
mango_account_0.perps[0]
|
||||||
.unsettled_pnl(&perp_market, I80F48::from(1200))
|
.unsettled_pnl(&perp_market, I80F48::from(1200))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
19980.0, // 1*100*(1200-1000) - (20 in fees)
|
19980.0, // 1*100*(1200-1000) - (20 in fees)
|
||||||
0.01
|
0.01
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
mango_account_1.perps[0]
|
mango_account_1.perps[0]
|
||||||
.unsettled_pnl(&perp_market, I80F48::from(1200))
|
.unsettled_pnl(&perp_market, I80F48::from(1200))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
-20000.0,
|
-20000.0,
|
||||||
0.01
|
0.01
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Difficult to test health due to fees being so small. Need alternative
|
// 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
|
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(
|
async fn try_bid(
|
||||||
&mut self,
|
&mut self,
|
||||||
limit_price: f64,
|
limit_price: f64,
|
||||||
max_base: u64,
|
max_base: u64,
|
||||||
taker: bool,
|
taker: bool,
|
||||||
) -> Result<mango_v4::accounts::Serum3PlaceOrder, TransportError> {
|
) -> Result<mango_v4::accounts::Serum3PlaceOrder, TransportError> {
|
||||||
let client_order_id = self.inc_client_order_id();
|
let ix = self.bid_ix(limit_price, max_base, taker);
|
||||||
let fees = if taker { 0.0004 } else { 0.0 };
|
send_tx(&self.solana, ix).await
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn bid_maker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
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)
|
.get_account::<Bank>(quote_bank)
|
||||||
.await
|
.await
|
||||||
.collected_fees_native;
|
.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
|
// check account2 balances too
|
||||||
context
|
context
|
||||||
|
@ -610,11 +614,11 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
||||||
.get_account::<Bank>(quote_bank)
|
.get_account::<Bank>(quote_bank)
|
||||||
.await
|
.await
|
||||||
.collected_fees_native;
|
.collected_fees_native;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
quote_fees3 - quote_fees1,
|
quote_fees3 - quote_fees1,
|
||||||
loan_origination_fee(fill_amount - deposit_amount) as f64,
|
loan_origination_fee(fill_amount - deposit_amount) as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
order_placer.settle().await;
|
order_placer.settle().await;
|
||||||
|
|
||||||
|
@ -623,11 +627,11 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
||||||
.get_account::<Bank>(quote_bank)
|
.get_account::<Bank>(quote_bank)
|
||||||
.await
|
.await
|
||||||
.collected_fees_native;
|
.collected_fees_native;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
quote_fees4 - quote_fees3,
|
quote_fees4 - quote_fees3,
|
||||||
serum_fee(fill_amount) as f64,
|
serum_fee(fill_amount) as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -720,11 +724,11 @@ async fn test_serum_settle_v1() -> Result<(), TransportError> {
|
||||||
.get_account::<Bank>(quote_bank)
|
.get_account::<Bank>(quote_bank)
|
||||||
.await
|
.await
|
||||||
.collected_fees_native;
|
.collected_fees_native;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
quote_fees_end - quote_fees_start,
|
quote_fees_end - quote_fees_start,
|
||||||
(lof + serum_referrer_fee(amount)) as f64,
|
(lof + serum_referrer_fee(amount)) as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -817,11 +821,11 @@ async fn test_serum_settle_v2_to_dao() -> Result<(), TransportError> {
|
||||||
.get_account::<Bank>(quote_bank)
|
.get_account::<Bank>(quote_bank)
|
||||||
.await
|
.await
|
||||||
.collected_fees_native;
|
.collected_fees_native;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
quote_fees_end - quote_fees_start,
|
quote_fees_end - quote_fees_start,
|
||||||
(lof + serum_referrer_fee(amount)) as f64,
|
(lof + serum_referrer_fee(amount)) as f64,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
|
|
||||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -913,11 +917,7 @@ async fn test_serum_settle_v2_to_account() -> Result<(), TransportError> {
|
||||||
.get_account::<Bank>(quote_bank)
|
.get_account::<Bank>(quote_bank)
|
||||||
.await
|
.await
|
||||||
.collected_fees_native;
|
.collected_fees_native;
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(quote_fees_end - quote_fees_start, lof as f64, 0.1);
|
||||||
quote_fees_end - quote_fees_start,
|
|
||||||
lof as f64,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
|
|
||||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
assert_eq!(account_data.buyback_fees_accrued_current, 0);
|
assert_eq!(account_data.buyback_fees_accrued_current, 0);
|
||||||
|
@ -1029,7 +1029,7 @@ async fn test_serum_reduce_only_deposits1() -> Result<(), TransportError> {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_serum_reduce_only_deposits2() -> Result<(), TransportError> {
|
async fn test_serum_reduce_only_deposits2() -> Result<(), TransportError> {
|
||||||
let mut test_builder = TestContextBuilder::new();
|
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 context = test_builder.start_default().await;
|
||||||
let solana = &context.solana.clone();
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
@ -1952,6 +1952,71 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
|
||||||
Ok(())
|
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 {
|
struct CommonSetup {
|
||||||
group_with_tokens: GroupWithTokens,
|
group_with_tokens: GroupWithTokens,
|
||||||
serum_market_cookie: SpotMarketCookie,
|
serum_market_cookie: SpotMarketCookie,
|
||||||
|
|
|
@ -321,7 +321,7 @@ async fn test_fallback_oracle_withdraw() -> Result<(), TransportError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_clmm_fallback_oracle() -> Result<(), TransportError> {
|
async fn test_orca_fallback_oracle() -> Result<(), TransportError> {
|
||||||
// add ability to find fixtures
|
// add ability to find fixtures
|
||||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
d.push("resources/test");
|
d.push("resources/test");
|
||||||
|
@ -340,6 +340,237 @@ async fn test_clmm_fallback_oracle() -> Result<(), TransportError> {
|
||||||
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
|
||||||
"FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH",
|
"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();
|
let fallback_oracle = Pubkey::from_str(fixtures[0].0).unwrap();
|
||||||
|
|
|
@ -2,8 +2,6 @@ use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
|
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 context = TestContext::new().await;
|
||||||
let solana = &context.solana.clone();
|
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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let liqee_base = account_position_f64(solana, account, base_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,
|
liqee_quote,
|
||||||
deposit_amount + 42.0, // roughly 50 / (1.1 * 1.1)
|
deposit_amount + 42.0, // roughly 50 / (1.1 * 1.1)
|
||||||
0.01
|
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_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(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_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
|
// 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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let liqee_base = account_position_f64(solana, account, base_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_eq_f64!(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_base, deposit_amount - 100.0, 0.01);
|
||||||
|
|
||||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(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_base, deposit_amount + 88.0, 0.01);
|
||||||
|
|
||||||
let account_data = get_mango_account(solana, account).await;
|
let account_data = get_mango_account(solana, account).await;
|
||||||
assert!(!account_data
|
assert!(!account_data
|
||||||
|
@ -334,8 +332,6 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportError> {
|
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 context = TestContext::new().await;
|
||||||
let solana = &context.solana.clone();
|
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
|
// TEST: Can't take an auction at any price when it's not started yet
|
||||||
//
|
//
|
||||||
|
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapTriggerInstruction {
|
TokenConditionalSwapTriggerInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -472,12 +468,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
||||||
min_buy_token: 0,
|
min_buy_token: 0,
|
||||||
min_taker_price: 0.0,
|
min_taker_price: 0.0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapNotStarted
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapNotStarted.into(),
|
|
||||||
"tcs should not be started yet".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||||
account_quote,
|
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||||
account_quote_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
assert!(assert_equal_f64_f64(
|
|
||||||
account_base,
|
|
||||||
account_base_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
|
|
||||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Stays at end price after end and before expiry
|
// 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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||||
account_quote,
|
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||||
account_quote_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
assert!(assert_equal_f64_f64(
|
|
||||||
account_base,
|
|
||||||
account_base_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
|
|
||||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Can't take when expired
|
// TEST: Can't take when expired
|
||||||
//
|
//
|
||||||
solana.set_clock_timestamp(initial_time + 22).await;
|
solana.set_clock_timestamp(initial_time + 22).await;
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapTriggerInstruction {
|
TokenConditionalSwapTriggerInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -582,12 +557,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
||||||
min_buy_token: 1,
|
min_buy_token: 1,
|
||||||
min_taker_price: 0.0,
|
min_taker_price: 0.0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapExpired
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapExpired.into(),
|
|
||||||
"tcs should be expired".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -595,8 +565,6 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportError> {
|
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 context = TestContext::new().await;
|
||||||
let solana = &context.solana.clone();
|
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;
|
set_bank_stub_oracle_price(solana, group, &base_token, admin, 10.0).await;
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapTriggerInstruction {
|
TokenConditionalSwapTriggerInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -732,15 +700,10 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
min_buy_token: 0,
|
min_buy_token: 0,
|
||||||
min_taker_price: 0.0,
|
min_taker_price: 0.0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapNotStarted
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapNotStarted.into(),
|
|
||||||
"not started yet".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapStartInstruction {
|
TokenConditionalSwapStartInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -748,19 +711,14 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
liqor_owner: owner,
|
liqor_owner: owner,
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapPriceNotInRange
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapPriceNotInRange.into(),
|
|
||||||
"price not in range".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Cannot trigger without start
|
// TEST: Cannot trigger without start
|
||||||
//
|
//
|
||||||
set_bank_stub_oracle_price(solana, group, &base_token, admin, 1.0).await;
|
set_bank_stub_oracle_price(solana, group, &base_token, admin, 1.0).await;
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapTriggerInstruction {
|
TokenConditionalSwapTriggerInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -772,12 +730,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
min_buy_token: 1,
|
min_buy_token: 1,
|
||||||
min_taker_price: 0.0,
|
min_taker_price: 0.0,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapNotStarted
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapNotStarted.into(),
|
|
||||||
"not started yet".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
send_tx(
|
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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||||
account_quote,
|
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||||
account_quote_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
assert!(assert_equal_f64_f64(
|
|
||||||
account_base,
|
|
||||||
account_base_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
|
|
||||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||||
|
|
||||||
let account_data = get_mango_account(solana, account).await;
|
let account_data = get_mango_account(solana, account).await;
|
||||||
let tcs = account_data
|
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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||||
account_quote,
|
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||||
account_quote_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
assert!(assert_equal_f64_f64(
|
|
||||||
account_base,
|
|
||||||
account_base_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
|
|
||||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Premium stops at max increases
|
// 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_quote = account_position_f64(solana, account, quote_token.bank).await;
|
||||||
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
let account_base = account_position_f64(solana, account, base_token.bank).await;
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
|
||||||
account_quote,
|
assert_eq_f64!(account_base, account_base_expected, 0.1);
|
||||||
account_quote_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
assert!(assert_equal_f64_f64(
|
|
||||||
account_base,
|
|
||||||
account_base_expected,
|
|
||||||
0.1
|
|
||||||
));
|
|
||||||
|
|
||||||
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||||
let liqor_base = account_position_f64(solana, liqor, base_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_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
|
||||||
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
|
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
|
||||||
|
|
||||||
//
|
//
|
||||||
// SETUP: make another premium auction to test starting
|
// 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
|
// TEST: Can't start if oracle not in range
|
||||||
//
|
//
|
||||||
|
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapStartInstruction {
|
TokenConditionalSwapStartInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -962,12 +891,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
liqor_owner: owner,
|
liqor_owner: owner,
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapPriceNotInRange
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapPriceNotInRange.into(),
|
|
||||||
"price is not in range".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -998,7 +922,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
// TEST: Can't start a second time
|
// TEST: Can't start a second time
|
||||||
//
|
//
|
||||||
|
|
||||||
let res = send_tx(
|
send_tx_expect_error!(
|
||||||
solana,
|
solana,
|
||||||
TokenConditionalSwapStartInstruction {
|
TokenConditionalSwapStartInstruction {
|
||||||
liqee: account,
|
liqee: account,
|
||||||
|
@ -1006,12 +930,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
liqor_owner: owner,
|
liqor_owner: owner,
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
)
|
MangoError::TokenConditionalSwapAlreadyStarted
|
||||||
.await;
|
|
||||||
assert_mango_error(
|
|
||||||
&res,
|
|
||||||
MangoError::TokenConditionalSwapAlreadyStarted.into(),
|
|
||||||
"already started".to_string(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1019,8 +938,6 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_token_conditional_swap_deposit_limit() -> Result<(), TransportError> {
|
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 context = TestContext::new().await;
|
||||||
let solana = &context.solana.clone();
|
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 interest_change = 5000.0 * (dynamic_rate + loan_fee_rate) * diff_ts / year;
|
||||||
let fee_change = 5000.0 * 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(),
|
bank_after.native_borrows() - bank_before.native_borrows(),
|
||||||
interest_change,
|
interest_change,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
bank_after.native_deposits() - bank_before.native_deposits(),
|
bank_after.native_deposits() - bank_before.native_deposits(),
|
||||||
interest_change,
|
interest_change,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(
|
assert_eq_fixed_f64!(
|
||||||
bank_after.collected_fees_native - bank_before.collected_fees_native,
|
bank_after.collected_fees_native - bank_before.collected_fees_native,
|
||||||
fee_change,
|
fee_change,
|
||||||
0.1
|
0.1
|
||||||
));
|
);
|
||||||
assert!(assert_equal(bank_after.avg_utilization, utilization, 0.01));
|
assert_eq_fixed_f64!(bank_after.avg_utilization, utilization, 0.01);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -140,19 +140,11 @@ async fn test_token_rates_migrate() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let bank_after = solana.get_account::<Bank>(tokens[0].bank).await;
|
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_eq_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_eq_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_eq_fixed_f64!(bank_after.max_rate, 0.5, 0.0001);
|
||||||
assert!(assert_equal_f64_f64(
|
assert_eq_f64!(bank_after.interest_curve_scaling, 3.0, 0.0001);
|
||||||
bank_after.interest_curve_scaling,
|
assert_eq_f64!(bank_after.interest_target_utilization as f64, 0.4, 0.0001);
|
||||||
3.0,
|
|
||||||
0.0001
|
|
||||||
));
|
|
||||||
assert!(assert_equal_f64_f64(
|
|
||||||
bank_after.interest_target_utilization as f64,
|
|
||||||
0.4,
|
|
||||||
0.0001
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,6 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use bytemuck::{bytes_of, Contiguous};
|
use bytemuck::{bytes_of, Contiguous};
|
||||||
use fixed::types::I80F48;
|
|
||||||
use solana_program::instruction::InstructionError;
|
use solana_program::instruction::InstructionError;
|
||||||
use solana_program::program_error::ProgramError;
|
use solana_program::program_error::ProgramError;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
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 {
|
#[macro_export]
|
||||||
let ok = (value.to_num::<f64>() - expected).abs() < max_error;
|
macro_rules! assert_eq_f64 {
|
||||||
if !ok {
|
($value:expr, $expected:expr, $max_error:expr $(,)?) => {
|
||||||
println!("comparison failed: value: {value}, expected: {expected}");
|
let value = $value;
|
||||||
}
|
let expected = $expected;
|
||||||
ok
|
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 {
|
#[macro_export]
|
||||||
let ok = (value - expected).abs() < max_error;
|
macro_rules! assert_eq_fixed_f64 {
|
||||||
if !ok {
|
($value:expr, $expected:expr, $max_error:expr $(,)?) => {
|
||||||
println!("comparison failed: value: {value}, expected: {expected}");
|
assert_eq_f64!($value.to_num::<f64>(), $expected, $max_error);
|
||||||
}
|
};
|
||||||
ok
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
market.serumMarketExternal,
|
||||||
);
|
);
|
||||||
console.log(
|
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()) {
|
for (const market of group.perpMarketsMapByMarketIndex.values()) {
|
||||||
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
|
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
|
||||||
console.log(
|
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()) {
|
for (const banks of group.banksMapByMint.values()) {
|
||||||
sig = await client.tokenDeregister(group, banks[0].mint);
|
sig = await client.tokenDeregister(group, banks[0].mint);
|
||||||
console.log(
|
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) {
|
for (const stubOracle of stubOracles) {
|
||||||
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
sig = await client.stubOracleClose(group, stubOracle.publicKey);
|
||||||
console.log(
|
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
|
// finally, close the group
|
||||||
sig = await client.groupClose(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();
|
process.exit();
|
||||||
|
|
|
@ -330,12 +330,12 @@ async function createAndPopulateAlt(
|
||||||
});
|
});
|
||||||
let sig = await client.sendAndConfirmTransaction([createIx[0]]);
|
let sig = await client.sendAndConfirmTransaction([createIx[0]]);
|
||||||
console.log(
|
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...`);
|
console.log(`ALT: set at index 0 for group...`);
|
||||||
sig = await client.altSet(group, createIx[1], 0);
|
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);
|
group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -366,7 +366,7 @@ async function createAndPopulateAlt(
|
||||||
addresses,
|
addresses,
|
||||||
});
|
});
|
||||||
const sig = await client.sendAndConfirmTransaction([extendIx]);
|
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
|
// Extend using mango v4 relevant pub keys
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import {
|
|
||||||
HealthType,
|
|
||||||
MangoAccount,
|
|
||||||
TokenPosition,
|
|
||||||
TokenPositionDto,
|
|
||||||
} from './mangoAccount';
|
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import { Bank, TokenIndex } from './bank';
|
|
||||||
import { deepClone, toNative, toUiDecimals } from '../utils';
|
|
||||||
import { expect } from 'chai';
|
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 { 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', () => {
|
describe('Mango Account', () => {
|
||||||
const mangoAccount = new MangoAccount(
|
const mangoAccount = new MangoAccount(
|
||||||
|
@ -32,6 +26,7 @@ describe('Mango Account', () => {
|
||||||
new BN(0),
|
new BN(0),
|
||||||
new BN(0),
|
new BN(0),
|
||||||
0,
|
0,
|
||||||
|
0,
|
||||||
new BN(0),
|
new BN(0),
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
|
@ -106,6 +101,7 @@ describe('maxWithdraw', () => {
|
||||||
new BN(0),
|
new BN(0),
|
||||||
new BN(0),
|
new BN(0),
|
||||||
0,
|
0,
|
||||||
|
0,
|
||||||
new BN(0),
|
new BN(0),
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
|
|
|
@ -51,6 +51,7 @@ export class MangoAccount {
|
||||||
buybackFeesAccruedCurrent: BN;
|
buybackFeesAccruedCurrent: BN;
|
||||||
buybackFeesAccruedPrevious: BN;
|
buybackFeesAccruedPrevious: BN;
|
||||||
buybackFeesExpiryTimestamp: BN;
|
buybackFeesExpiryTimestamp: BN;
|
||||||
|
sequenceNumber: number;
|
||||||
headerVersion: number;
|
headerVersion: number;
|
||||||
tokens: unknown;
|
tokens: unknown;
|
||||||
serum3: unknown;
|
serum3: unknown;
|
||||||
|
@ -76,6 +77,7 @@ export class MangoAccount {
|
||||||
obj.buybackFeesAccruedCurrent,
|
obj.buybackFeesAccruedCurrent,
|
||||||
obj.buybackFeesAccruedPrevious,
|
obj.buybackFeesAccruedPrevious,
|
||||||
obj.buybackFeesExpiryTimestamp,
|
obj.buybackFeesExpiryTimestamp,
|
||||||
|
obj.sequenceNumber,
|
||||||
obj.headerVersion,
|
obj.headerVersion,
|
||||||
obj.lastCollateralFeeCharge,
|
obj.lastCollateralFeeCharge,
|
||||||
obj.tokens as TokenPositionDto[],
|
obj.tokens as TokenPositionDto[],
|
||||||
|
@ -103,6 +105,7 @@ export class MangoAccount {
|
||||||
public buybackFeesAccruedCurrent: BN,
|
public buybackFeesAccruedCurrent: BN,
|
||||||
public buybackFeesAccruedPrevious: BN,
|
public buybackFeesAccruedPrevious: BN,
|
||||||
public buybackFeesExpiryTimestamp: BN,
|
public buybackFeesExpiryTimestamp: BN,
|
||||||
|
public sequenceNumber: number,
|
||||||
public headerVersion: number,
|
public headerVersion: number,
|
||||||
public lastCollateralFeeCharge: BN,
|
public lastCollateralFeeCharge: BN,
|
||||||
tokens: TokenPositionDto[],
|
tokens: TokenPositionDto[],
|
||||||
|
|
|
@ -83,7 +83,7 @@ import {
|
||||||
import { Id } from './ids';
|
import { Id } from './ids';
|
||||||
import { IDL, MangoV4 } from './mango_v4';
|
import { IDL, MangoV4 } from './mango_v4';
|
||||||
import { I80F48 } from './numbers/I80F48';
|
import { I80F48 } from './numbers/I80F48';
|
||||||
import { FlashLoanType, OracleConfigParams } from './types';
|
import { FlashLoanType, HealthCheckKind, OracleConfigParams } from './types';
|
||||||
import {
|
import {
|
||||||
I64_MAX_BN,
|
I64_MAX_BN,
|
||||||
U64_MAX_BN,
|
U64_MAX_BN,
|
||||||
|
@ -1056,6 +1056,50 @@ export class MangoClient {
|
||||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
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(
|
public async getMangoAccount(
|
||||||
mangoAccountPk: PublicKey,
|
mangoAccountPk: PublicKey,
|
||||||
loadSerum3Oo = false,
|
loadSerum3Oo = false,
|
||||||
|
|
|
@ -310,6 +310,8 @@ export interface IxGateParams {
|
||||||
TokenConditionalSwapCreateLinearAuction: boolean;
|
TokenConditionalSwapCreateLinearAuction: boolean;
|
||||||
Serum3PlaceOrderV2: boolean;
|
Serum3PlaceOrderV2: boolean;
|
||||||
TokenForceWithdraw: boolean;
|
TokenForceWithdraw: boolean;
|
||||||
|
SequenceCheck: boolean;
|
||||||
|
HealthCheck: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default with all ixs enabled, use with buildIxGate
|
// Default with all ixs enabled, use with buildIxGate
|
||||||
|
@ -390,6 +392,8 @@ export const TrueIxGateParams: IxGateParams = {
|
||||||
TokenConditionalSwapCreateLinearAuction: true,
|
TokenConditionalSwapCreateLinearAuction: true,
|
||||||
Serum3PlaceOrderV2: true,
|
Serum3PlaceOrderV2: true,
|
||||||
TokenForceWithdraw: true,
|
TokenForceWithdraw: true,
|
||||||
|
SequenceCheck: true,
|
||||||
|
HealthCheck: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
||||||
|
@ -480,6 +484,8 @@ export function buildIxGate(p: IxGateParams): BN {
|
||||||
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
||||||
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
||||||
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
|
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
|
||||||
|
toggleIx(ixGate, p, 'SequenceCheck', 73);
|
||||||
|
toggleIx(ixGate, p, 'HealthCheck', 74);
|
||||||
|
|
||||||
return ixGate;
|
return ixGate;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export type MangoV4 = {
|
export type MangoV4 = {
|
||||||
"version": "0.23.0",
|
"version": "0.24.0",
|
||||||
"name": "mango_v4",
|
"name": "mango_v4",
|
||||||
"instructions": [
|
"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",
|
"name": "stubOracleCreate",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
|
@ -7871,13 +7931,8 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "sequenceNumber",
|
||||||
"type": {
|
"type": "u8"
|
||||||
"array": [
|
|
||||||
"u8",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "netDeposits",
|
"name": "netDeposits",
|
||||||
|
@ -9669,13 +9724,8 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "sequenceNumber",
|
||||||
"type": {
|
"type": "u8"
|
||||||
"array": [
|
|
||||||
"u8",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "netDeposits",
|
"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",
|
"name": "Serum3SelfTradeBehavior",
|
||||||
"docs": [
|
"docs": [
|
||||||
|
@ -11008,6 +11084,12 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenForceWithdraw"
|
"name": "TokenForceWithdraw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SequenceCheck"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HealthCheck"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11048,6 +11130,9 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "OrcaCLMM"
|
"name": "OrcaCLMM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RaydiumCLMM"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -14347,12 +14432,27 @@ export type MangoV4 = {
|
||||||
"code": 6069,
|
"code": 6069,
|
||||||
"name": "TokenAssetLiquidationDisabled",
|
"name": "TokenAssetLiquidationDisabled",
|
||||||
"msg": "the asset does not allow liquidation"
|
"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 = {
|
export const IDL: MangoV4 = {
|
||||||
"version": "0.23.0",
|
"version": "0.24.0",
|
||||||
"name": "mango_v4",
|
"name": "mango_v4",
|
||||||
"instructions": [
|
"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",
|
"name": "stubOracleCreate",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
|
@ -22224,13 +22384,8 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "sequenceNumber",
|
||||||
"type": {
|
"type": "u8"
|
||||||
"array": [
|
|
||||||
"u8",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "netDeposits",
|
"name": "netDeposits",
|
||||||
|
@ -24022,13 +24177,8 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "sequenceNumber",
|
||||||
"type": {
|
"type": "u8"
|
||||||
"array": [
|
|
||||||
"u8",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "netDeposits",
|
"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",
|
"name": "Serum3SelfTradeBehavior",
|
||||||
"docs": [
|
"docs": [
|
||||||
|
@ -25361,6 +25537,12 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenForceWithdraw"
|
"name": "TokenForceWithdraw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SequenceCheck"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HealthCheck"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -25401,6 +25583,9 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "OrcaCLMM"
|
"name": "OrcaCLMM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RaydiumCLMM"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -28700,6 +28885,21 @@ export const IDL: MangoV4 = {
|
||||||
"code": 6069,
|
"code": 6069,
|
||||||
"name": "TokenAssetLiquidationDisabled",
|
"name": "TokenAssetLiquidationDisabled",
|
||||||
"msg": "the asset does not allow liquidation"
|
"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 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 {
|
export class InterestRateParams {
|
||||||
util0: number;
|
util0: number;
|
||||||
rate0: number;
|
rate0: number;
|
||||||
|
|
Loading…
Reference in New Issue