Merge branch 'main' into deploy

This commit is contained in:
microwavedcola1 2024-04-18 15:15:44 +02:00
commit fb6311e842
77 changed files with 3610 additions and 940 deletions

View File

@ -4,7 +4,33 @@ Update this for each program release and mainnet deployment.
## not on mainnet
### v0.23.0, 2024-3-
### v0.24.0, 2024-4-
- Allow skipping banks and invalid oracles when computing health (#891)
This is only possible when we know for sure that the operation would not put the account into negative health zone.
- Add support for Raydium CLMM as oracle fallback (#856)
- Add a `TokenBalanceLog` when charging collateral fees (#894)
- Withdraw instruction: remove overflow error and return appropriate error message instead (#910)
- Banks: add more safety checks (#895)
- Add a health check instruction (#913)
Assert in a transaction that operation run on a mango account does not reduce it's health below a specified amount.
- Add a sequence check instruction (#909)
Assert that a transaction was emitted and run with a correct view of the current mango state.
## mainnet
### v0.23.0, 2024-3-8
Deployment: Mar 8, 2024 at 12:10:52 Central European Standard Time, https://explorer.solana.com/tx/6MXGookZoYGMYb7tWrrmgZzVA13HJimHNqwHRVFeqL9YpQD7YasH1pQn4MSQTK1o13ixKTGFxwZsviUzmHzzP9m
- Allow disabling asset liquidations for tokens (#867)
@ -26,8 +52,6 @@ Update this for each program release and mainnet deployment.
- Flash loan: Add a "swap without flash loan fees" option (#882)
- Cleanup, tests and minor (#878, #875, #854, #838, #895)
## mainnet
### v0.22.0, 2024-3-3
Deployment: Mar 3, 2024 at 23:52:08 Central European Standard Time, https://explorer.solana.com/tx/3MpEMU12Pv7RpSnwfShoM9sbyr41KAEeJFCVx9ypkq8nuK8Q5vm7CRLkdhH3u91yQ4k44a32armZHaoYguX6NqsY

20
Cargo.lock generated
View File

@ -2455,6 +2455,20 @@ version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "hdrhistogram"
version = "7.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
dependencies = [
"base64 0.21.4",
"byteorder",
"crossbeam-channel",
"flate2 1.0.27",
"nom 7.1.3",
"num-traits",
]
[[package]]
name = "headers"
version = "0.3.9"
@ -3367,15 +3381,17 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.23.0"
version = "0.24.0"
dependencies = [
"anchor-lang",
"anchor-spl",
"anyhow",
"arrayref",
"async-trait",
"base64 0.13.1",
"bincode",
"borsh 0.10.3",
"bs58 0.5.0",
"bytemuck",
"default-env",
"derivative",
@ -3391,6 +3407,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"serde",
"serde_json",
"serum_dex 0.5.10 (git+https://github.com/openbook-dex/program.git)",
"solana-address-lookup-table-program",
"solana-logger",
@ -3522,6 +3539,7 @@ dependencies = [
"futures 0.3.28",
"futures-core",
"futures-util",
"hdrhistogram",
"itertools",
"jemallocator",
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -7,19 +7,18 @@
- 4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg is the address of the Mango v4 Program
- FP4PxqHTVzeG2c6eZd7974F9WvKUSdBeduUK3rjYyvBw is the address of the Mango v4 Program Governance
- Check out the latest version of the `dev` branch
- Assuming there's a release branch (like release/program-v0.22.0)
with a completed audit and an updated changelog.
- Update the changelog
git log program-v0.11.0..HEAD -- programs/mango-v4/
- Check out the release branch
- Make sure the version is bumped in programs/mango-v4/Cargo.toml
- Update the idl ./update-local-idl.sh
- Update the idl ./update-local-idl.sh and verify that there's no difference
- Run the tests to double check
- Run the tests to double check there are no failures
- Tag and push
- Tag (`git tag program-v0.xy.z HEAD`) and push it (`git push <tag>`)
- Do a verifiable build

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -28,12 +28,13 @@ pub async fn save_snapshot(
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
let oracles_and_vaults = group_context
let extra_accounts = group_context
.tokens
.values()
.map(|value| value.oracle)
.chain(group_context.perp_markets.values().map(|p| p.oracle))
.chain(group_context.tokens.values().flat_map(|value| value.vaults))
.chain(group_context.address_lookup_tables.iter().copied())
.unique()
.filter(|pk| *pk != Pubkey::default())
.collect::<Vec<Pubkey>>();
@ -55,7 +56,7 @@ pub async fn save_snapshot(
serum_programs,
open_orders_authority: mango_group,
},
oracles_and_vaults.clone(),
extra_accounts.clone(),
account_update_sender.clone(),
);
@ -75,7 +76,7 @@ pub async fn save_snapshot(
snapshot_interval: Duration::from_secs(6000),
min_slot: first_websocket_slot + 10,
},
oracles_and_vaults,
extra_accounts,
account_update_sender,
);
tokio::spawn(async move {

View File

@ -49,3 +49,4 @@ tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.16.1"
tracing = "0.1"
regex = "1.9.5"
hdrhistogram = "7.5.4"

View File

@ -287,6 +287,8 @@ async fn main() -> anyhow::Result<()> {
let mut metric_account_update_queue_len =
metrics.register_u64("account_update_queue_length".into());
let mut metric_chain_update_latency =
metrics.register_latency("in-memory chain update".into());
let mut metric_mango_accounts = metrics.register_u64("mango_accounts".into());
let mut mint_infos = HashMap::<TokenIndex, Pubkey>::new();
@ -299,6 +301,7 @@ async fn main() -> anyhow::Result<()> {
.recv()
.await
.expect("channel not closed");
let current_time = Instant::now();
metric_account_update_queue_len.set(account_update_receiver.len() as u64);
message.update_chain_data(&mut chain_data.write().unwrap());
@ -306,6 +309,15 @@ async fn main() -> anyhow::Result<()> {
match message {
Message::Account(account_write) => {
let mut state = shared_state.write().unwrap();
let reception_time = account_write.reception_time;
state.oldest_chain_event_reception_time = Some(
state
.oldest_chain_event_reception_time
.unwrap_or(reception_time),
);
metric_chain_update_latency.push(current_time - reception_time);
if is_mango_account(&account_write.account, &mango_group).is_some() {
// e.g. to render debug logs RUST_LOG="liquidator=debug"
debug!(
@ -320,8 +332,21 @@ async fn main() -> anyhow::Result<()> {
}
Message::Snapshot(snapshot) => {
let mut state = shared_state.write().unwrap();
let mut reception_time = None;
// Track all mango account pubkeys
for update in snapshot.iter() {
reception_time = Some(
update
.reception_time
.min(reception_time.unwrap_or(update.reception_time)),
);
state.oldest_chain_event_reception_time = Some(
state
.oldest_chain_event_reception_time
.unwrap_or(update.reception_time),
);
if is_mango_account(&update.account, &mango_group).is_some() {
state.mango_accounts.insert(update.pubkey);
}
@ -335,6 +360,11 @@ async fn main() -> anyhow::Result<()> {
oracles.insert(perp_market.oracle);
}
}
if reception_time.is_some() {
metric_chain_update_latency
.push(current_time - reception_time.unwrap());
}
metric_mango_accounts.set(state.mango_accounts.len() as u64);
state.one_snapshot_done = true;
@ -374,35 +404,82 @@ async fn main() -> anyhow::Result<()> {
let liquidation_job = tokio::spawn({
let mut interval =
mango_v4_client::delay_interval(Duration::from_millis(cli.check_interval_ms));
let mut metric_liquidation_check = metrics.register_latency("liquidation_check".into());
let mut metric_liquidation_start_end =
metrics.register_latency("liquidation_start_end".into());
let mut liquidation_start_time = None;
let mut tcs_start_time = None;
let shared_state = shared_state.clone();
async move {
loop {
interval.tick().await;
let account_addresses = {
let state = shared_state.write().unwrap();
let mut state = shared_state.write().unwrap();
if !state.one_snapshot_done {
// discard first latency info as it will skew data too much
state.oldest_chain_event_reception_time = None;
continue;
}
if state.oldest_chain_event_reception_time.is_none()
&& liquidation_start_time.is_none()
{
// no new update, skip computing
continue;
}
state.mango_accounts.iter().cloned().collect_vec()
};
liquidation.errors.update();
liquidation.oracle_errors.update();
if liquidation_start_time.is_none() {
liquidation_start_time = Some(Instant::now());
}
let liquidated = liquidation
.maybe_liquidate_one(account_addresses.iter())
.await;
if !liquidated {
// This will be incorrect if we liquidate the last checked account
// (We will wait for next full run, skewing latency metrics)
// Probability is very low, might not need to be fixed
let mut state = shared_state.write().unwrap();
let reception_time = state.oldest_chain_event_reception_time.unwrap();
let current_time = Instant::now();
state.oldest_chain_event_reception_time = None;
metric_liquidation_check.push(current_time - reception_time);
metric_liquidation_start_end
.push(current_time - liquidation_start_time.unwrap());
liquidation_start_time = None;
}
let mut took_tcs = false;
if !liquidated && cli.take_tcs == BoolArg::True {
tcs_start_time = Some(tcs_start_time.unwrap_or(Instant::now()));
took_tcs = liquidation
.maybe_take_token_conditional_swap(account_addresses.iter())
.await
.unwrap_or_else(|err| {
error!("error during maybe_take_token_conditional_swap: {err}");
false
})
});
if !took_tcs {
let current_time = Instant::now();
let mut metric_tcs_start_end =
metrics.register_latency("tcs_start_end".into());
metric_tcs_start_end.push(current_time - tcs_start_time.unwrap());
tcs_start_time = None;
}
}
if liquidated || took_tcs {
@ -483,6 +560,9 @@ struct SharedState {
/// Is the first snapshot done? Only start checking account health when it is.
one_snapshot_done: bool,
/// Oldest chain event not processed yet
oldest_chain_event_reception_time: Option<Instant>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View File

@ -1,3 +1,5 @@
use hdrhistogram::Histogram;
use std::time::Duration;
use {
std::collections::HashMap,
std::sync::{atomic, Arc, Mutex, RwLock},
@ -10,6 +12,7 @@ enum Value {
U64(Arc<atomic::AtomicU64>),
I64(Arc<atomic::AtomicI64>),
String(Arc<Mutex<String>>),
Latency(Arc<Mutex<Histogram<u64>>>),
}
#[derive(Debug)]
@ -49,6 +52,18 @@ impl MetricU64 {
}
}
#[derive(Clone)]
pub struct MetricLatency {
value: Arc<Mutex<Histogram<u64>>>,
}
impl MetricLatency {
pub fn push(&mut self, duration: std::time::Duration) {
let mut guard = self.value.lock().unwrap();
let ns: u64 = duration.as_nanos().try_into().unwrap();
guard.record(ns).expect("latency error");
}
}
#[derive(Clone)]
pub struct MetricI64 {
value: Arc<atomic::AtomicI64>,
@ -110,6 +125,19 @@ impl Metrics {
}
}
pub fn register_latency(&self, name: String) -> MetricLatency {
let mut registry = self.registry.write().unwrap();
let value = registry.entry(name).or_insert_with(|| {
Value::Latency(Arc::new(Mutex::new(Histogram::<u64>::new(3).unwrap())))
});
MetricLatency {
value: match value {
Value::Latency(v) => v.clone(),
_ => panic!("bad metric type"),
},
}
}
pub fn register_string(&self, name: String) -> MetricString {
let mut registry = self.registry.write().unwrap();
let value = registry
@ -187,6 +215,16 @@ pub fn start() -> Metrics {
);
}
}
Value::Latency(v) => {
let hist = v.lock().unwrap();
info!(
"metric: {}: 99'th percentile: {:?}, 99,9'th percentile: {:?}",
name,
Duration::from_nanos(hist.value_at_quantile(0.99)),
Duration::from_nanos(hist.value_at_quantile(0.999))
);
}
}
}
}

View File

@ -1,6 +1,7 @@
use solana_client::rpc_response::{Response, RpcKeyedAccount};
use solana_sdk::{account::AccountSharedData, pubkey::Pubkey};
use std::time::Instant;
use std::{str::FromStr, sync::Arc};
use tracing::*;
@ -11,6 +12,7 @@ pub struct AccountUpdate {
pub pubkey: Pubkey,
pub slot: u64,
pub account: AccountSharedData,
pub reception_time: Instant,
}
impl AccountUpdate {
@ -25,15 +27,22 @@ impl AccountUpdate {
pubkey,
slot: rpc.context.slot,
account,
reception_time: Instant::now(),
})
}
}
#[derive(Clone)]
pub struct ChainSlotUpdate {
pub slot_update: Arc<solana_client::rpc_response::SlotUpdate>,
pub reception_time: Instant,
}
#[derive(Clone)]
pub enum Message {
Account(AccountUpdate),
Snapshot(Vec<AccountUpdate>),
Slot(Arc<solana_client::rpc_response::SlotUpdate>),
Slot(ChainSlotUpdate),
}
impl Message {
@ -65,7 +74,7 @@ impl Message {
}
Message::Slot(slot_update) => {
trace!("websocket slot message");
let slot_update = match **slot_update {
let slot_update = match *(slot_update.slot_update) {
solana_client::rpc_response::SlotUpdate::CreatedBank {
slot, parent, ..
} => Some(SlotData {

View File

@ -7,9 +7,9 @@ use anchor_lang::__private::bytemuck;
use mango_v4::{
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
state::{
determine_oracle_type, load_whirlpool_state, oracle_state_unchecked, Group,
MangoAccountValue, OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType,
PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
determine_oracle_type, load_orca_pool_state, load_raydium_pool_state,
oracle_state_unchecked, Group, MangoAccountValue, OracleAccountInfos, OracleConfig,
OracleConfigParams, OracleType, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
},
};
@ -721,10 +721,14 @@ async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Acco
fn get_fallback_quote_key(acc_info: &impl KeyedAccountReader) -> Pubkey {
let maybe_key = match determine_oracle_type(acc_info).ok() {
Some(oracle_type) => match oracle_type {
OracleType::OrcaCLMM => match load_whirlpool_state(acc_info).ok() {
OracleType::OrcaCLMM => match load_orca_pool_state(acc_info).ok() {
Some(whirlpool) => whirlpool.get_quote_oracle().ok(),
None => None,
},
OracleType::RaydiumCLMM => match load_raydium_pool_state(acc_info).ok() {
Some(pool) => pool.get_quote_oracle().ok(),
None => None,
},
_ => None,
},
None => None,

View File

@ -15,8 +15,7 @@ use futures::{stream, StreamExt};
use solana_rpc::rpc::rpc_accounts::AccountsDataClient;
use solana_rpc::rpc::rpc_accounts_scan::AccountsScanClient;
use std::str::FromStr;
use std::time::Duration;
use tokio::task::JoinHandle;
use std::time::{Duration, Instant};
use tokio::time;
use tracing::*;
@ -56,6 +55,7 @@ impl AccountSnapshot {
.account
.decode()
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
reception_time: Instant::now(),
});
}
Ok(())
@ -75,6 +75,7 @@ impl AccountSnapshot {
account: ui_account
.decode()
.ok_or_else(|| anyhow::anyhow!("could not decode account"))?,
reception_time: Instant::now(),
});
}
}

View File

@ -11,11 +11,11 @@ use solana_rpc::rpc_pubsub::RpcSolPubSubClient;
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
use anyhow::Context;
use std::time::Duration;
use std::time::{Duration, Instant};
use tokio_stream::StreamMap;
use tracing::*;
use crate::account_update_stream::{AccountUpdate, Message};
use crate::account_update_stream::{AccountUpdate, ChainSlotUpdate, Message};
use crate::AnyhowWrap;
pub struct Config {
@ -143,7 +143,10 @@ async fn feed_data(
},
message = slot_sub.next() => {
if let Some(data) = message {
sender.send(Message::Slot(data.map_err_anyhow()?)).await.expect("sending must succeed");
sender.send(Message::Slot(ChainSlotUpdate{
slot_update: data.map_err_anyhow()?,
reception_time: Instant::now()
})).await.expect("sending must succeed");
} else {
warn!("slot update stream closed");
return Ok(());
@ -200,7 +203,7 @@ pub async fn get_next_create_bank_slot(
match msg {
Message::Slot(slot_update) => {
if let solana_client::rpc_response::SlotUpdate::CreatedBank { slot, .. } =
*slot_update
*slot_update.slot_update
{
return Ok(slot);
}

View File

@ -1,5 +1,5 @@
{
"version": "0.23.0",
"version": "0.24.0",
"name": "mango_v4",
"instructions": [
{
@ -1760,6 +1760,66 @@
}
]
},
{
"name": "sequenceCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group",
"owner"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
}
],
"args": [
{
"name": "expectedSequenceNumber",
"type": "u8"
}
]
},
{
"name": "healthCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": [
{
"name": "minHealthValue",
"type": "f64"
},
{
"name": "checkKind",
"type": {
"defined": "HealthCheckKind"
}
}
]
},
{
"name": "stubOracleCreate",
"accounts": [
@ -7871,13 +7931,8 @@
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
"name": "sequenceNumber",
"type": "u8"
},
{
"name": "netDeposits",
@ -9669,13 +9724,8 @@
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
"name": "sequenceNumber",
"type": "u8"
},
{
"name": "netDeposits",
@ -10654,6 +10704,32 @@
]
}
},
{
"name": "HealthCheckKind",
"type": {
"kind": "enum",
"variants": [
{
"name": "Maint"
},
{
"name": "Init"
},
{
"name": "LiquidationEnd"
},
{
"name": "MaintRatio"
},
{
"name": "InitRatio"
},
{
"name": "LiquidationEndRatio"
}
]
}
},
{
"name": "Serum3SelfTradeBehavior",
"docs": [
@ -11008,6 +11084,12 @@
},
{
"name": "TokenForceWithdraw"
},
{
"name": "SequenceCheck"
},
{
"name": "HealthCheck"
}
]
}
@ -11048,6 +11130,9 @@
},
{
"name": "OrcaCLMM"
},
{
"name": "RaydiumCLMM"
}
]
}
@ -14347,6 +14432,21 @@
"code": 6069,
"name": "TokenAssetLiquidationDisabled",
"msg": "the asset does not allow liquidation"
},
{
"code": 6070,
"name": "BorrowsRequireHealthAccountBank",
"msg": "for borrows the bank must be in the health account list"
},
{
"code": 6071,
"name": "InvalidSequenceNumber",
"msg": "invalid sequence number"
},
{
"code": 6072,
"name": "InvalidHealth",
"msg": "invalid health"
}
]
}

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.23.0"
version = "0.24.0"
description = "Created with Anchor"
edition = "2021"
@ -75,3 +75,6 @@ rand = "0.8.4"
lazy_static = "1.4.0"
num = "0.4.0"
regex = "1"
serde_json = "1"
bs58 = "0.5"
anyhow = "1"

View File

@ -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>,
}

View File

@ -16,6 +16,7 @@ pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use group_withdraw_insurance_fund::*;
pub use health_check::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use openbook_v2_cancel_order::*;
@ -45,6 +46,7 @@ pub use perp_place_order::*;
pub use perp_settle_fees::*;
pub use perp_settle_pnl::*;
pub use perp_update_funding::*;
pub use sequence_check::*;
pub use serum3_cancel_all_orders::*;
pub use serum3_cancel_order::*;
pub use serum3_close_open_orders::*;
@ -94,6 +96,7 @@ mod group_close;
mod group_create;
mod group_edit;
mod group_withdraw_insurance_fund;
mod health_check;
mod health_region;
mod ix_gate_set;
mod openbook_v2_cancel_order;
@ -123,6 +126,7 @@ mod perp_place_order;
mod perp_settle_fees;
mod perp_settle_pnl;
mod perp_update_funding;
mod sequence_check;
mod serum3_cancel_all_orders;
mod serum3_cancel_order;
mod serum3_close_open_orders;

View File

@ -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>,
}

View File

@ -13,6 +13,10 @@ pub trait AccountReader {
fn data(&self) -> &[u8];
}
pub trait AccountDataWriter {
fn data_as_mut_slice(&mut self) -> &mut [u8];
}
/// Like AccountReader, but can also get the account pubkey
pub trait KeyedAccountReader: AccountReader {
fn key(&self) -> &Pubkey;
@ -99,6 +103,12 @@ impl<'info, 'a> KeyedAccountReader for AccountInfoRefMut<'info, 'a> {
}
}
impl<'info, 'a> AccountDataWriter for AccountInfoRefMut<'info, 'a> {
fn data_as_mut_slice(&mut self) -> &mut [u8] {
&mut self.data
}
}
#[cfg(feature = "solana-sdk")]
impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
fn owner(&self) -> &Pubkey {
@ -110,6 +120,13 @@ impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
}
}
#[cfg(feature = "solana-sdk")]
impl<T: solana_sdk::account::WritableAccount> AccountDataWriter for T {
fn data_as_mut_slice(&mut self) -> &mut [u8] {
self.data_as_mut_slice()
}
}
#[cfg(feature = "solana-sdk")]
#[derive(Clone)]
pub struct KeyedAccount {
@ -232,28 +249,29 @@ impl<A: AccountReader> LoadZeroCopy for A {
}
}
impl<'info, 'a> LoadMutZeroCopy for AccountInfoRefMut<'info, 'a> {
impl<A: AccountReader + AccountDataWriter> LoadMutZeroCopy for A {
fn load_mut<T: ZeroCopy + Owner>(&mut self) -> Result<&mut T> {
if self.owner != &T::owner() {
if self.owner() != &T::owner() {
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
}
if self.data.len() < 8 {
let data = self.data_as_mut_slice();
if data.len() < 8 {
return Err(ErrorCode::AccountDiscriminatorNotFound.into());
}
let disc_bytes = array_ref![self.data, 0, 8];
let disc_bytes = array_ref![data, 0, 8];
if disc_bytes != &T::discriminator() {
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
}
Ok(bytemuck::from_bytes_mut(
&mut self.data[8..mem::size_of::<T>() + 8],
&mut data[8..mem::size_of::<T>() + 8],
))
}
fn load_mut_fully_unchecked<T: ZeroCopy + Owner>(&mut self) -> Result<&mut T> {
Ok(bytemuck::from_bytes_mut(
&mut self.data[8..mem::size_of::<T>() + 8],
&mut self.data_as_mut_slice()[8..mem::size_of::<T>() + 8],
))
}
}

View File

@ -145,6 +145,12 @@ pub enum MangoError {
MissingFeedForCLMMOracle,
#[msg("the asset does not allow liquidation")]
TokenAssetLiquidationDisabled,
#[msg("for borrows the bank must be in the health account list")]
BorrowsRequireHealthAccountBank,
#[msg("invalid sequence number")]
InvalidSequenceNumber,
#[msg("invalid health")]
InvalidHealth,
}
impl MangoError {

View File

@ -26,6 +26,9 @@ use crate::state::{Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, TokenInde
/// are passed because health needs to be computed for different baskets in
/// one instruction (such as for liquidation instructions).
pub trait AccountRetriever {
/// Returns the token indexes of the available banks. Unordered and may have duplicates.
fn available_banks(&self) -> Result<Vec<TokenIndex>>;
fn bank_and_oracle(
&self,
group: &Pubkey,
@ -45,11 +48,12 @@ pub trait AccountRetriever {
/// Assumes the account infos needed for the health computation follow a strict order.
///
/// 1. n_banks Bank account, in the order of account.token_iter_active()
/// 1. n_banks Bank account, in the order of account.active_token_positions() although it's
/// allowed for some of the banks (and their oracles in 2.) to be skipped
/// 2. n_banks oracle accounts, one for each bank in the same order
/// 3. PerpMarket accounts, in the order of account.perps.iter_active_accounts()
/// 3. PerpMarket accounts, in the order of account.perps.active_perp_positions()
/// 4. PerpMarket oracle accounts, in the order of the perp market accounts
/// 5. serum3 OpenOrders accounts, in the order of account.serum3.iter_active()
/// 5. serum3 OpenOrders accounts, in the order of account.active_serum3_orders()
/// 6. fallback oracle accounts, order and existence of accounts is not guaranteed
pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub ais: Vec<T>,
@ -63,20 +67,67 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub sol_oracle_index: Option<usize>,
}
/// Creates a FixedOrderAccountRetriever where all banks are present
///
/// Note that this does not eagerly validate that the right accounts were passed. That
/// validation happens only when banks, perps etc are requested.
pub fn new_fixed_order_account_retriever<'a, 'info>(
ais: &'a [AccountInfo<'info>],
account: &MangoAccountRef,
now_slot: u64,
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
let active_token_len = account.active_token_positions().count();
// Load the banks early to verify them
for ai in &ais[0..active_token_len] {
ai.load::<Bank>()?;
}
new_fixed_order_account_retriever_inner(ais, account, now_slot, active_token_len)
}
/// A FixedOrderAccountRetriever with n_banks <= active_token_positions().count(),
/// depending on which banks were passed.
///
/// Note that this does not eagerly validate that the right accounts were passed. That
/// validation happens only when banks, perps etc are requested.
pub fn new_fixed_order_account_retriever_with_optional_banks<'a, 'info>(
ais: &'a [AccountInfo<'info>],
account: &MangoAccountRef,
now_slot: u64,
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
// Scan for the number of banks provided
let mut n_banks = 0;
for ai in ais {
if let Some((_, bank_result)) = can_load_as::<Bank>((0, ai)) {
bank_result?;
n_banks += 1;
} else {
break;
}
}
let active_token_len = account.active_token_positions().count();
require_gte!(active_token_len, n_banks);
new_fixed_order_account_retriever_inner(ais, account, now_slot, n_banks)
}
pub fn new_fixed_order_account_retriever_inner<'a, 'info>(
ais: &'a [AccountInfo<'info>],
account: &MangoAccountRef,
now_slot: u64,
n_banks: usize,
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
let active_serum3_len = account.active_serum3_orders().count();
let active_perp_len = account.active_perp_positions().count();
let expected_ais = active_token_len * 2 // banks + oracles
let expected_ais = n_banks * 2 // banks + oracles
+ active_perp_len * 2 // PerpMarkets + Oracles
+ active_serum3_len; // open_orders
require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount,
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
ais.len(), expected_ais,
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len
n_banks, n_banks, active_perp_len, active_perp_len, active_serum3_len
);
let usdc_oracle_index = ais[..]
.iter()
@ -87,11 +138,11 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
Ok(FixedOrderAccountRetriever {
ais: AccountInfoRef::borrow_slice(ais)?,
n_banks: active_token_len,
n_banks,
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: Some(Clock::get()?.slot),
begin_perp: n_banks * 2,
begin_serum3: n_banks * 2 + active_perp_len * 2,
staleness_slot: Some(now_slot),
begin_fallback_oracles: expected_ais,
usdc_oracle_index,
sol_oracle_index,
@ -99,11 +150,28 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
}
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
fn bank(&self, group: &Pubkey, account_index: usize, token_index: TokenIndex) -> Result<&Bank> {
let bank = self.ais[account_index].load::<Bank>()?;
require_keys_eq!(bank.group, *group);
require_eq!(bank.token_index, token_index);
Ok(bank)
fn bank(
&self,
group: &Pubkey,
active_token_position_index: usize,
token_index: TokenIndex,
) -> Result<(usize, &Bank)> {
// Maybe not all banks were passed: The desired bank must be at or
// to the left of account_index and left of n_banks.
let end_index = (active_token_position_index + 1).min(self.n_banks);
for i in (0..end_index).rev() {
let ai = &self.ais[i];
let bank = ai.load_fully_unchecked::<Bank>()?;
if bank.token_index == token_index {
require_keys_eq!(bank.group, *group);
return Ok((i, bank));
}
}
Err(error_msg_typed!(
MangoError::InvalidHealthAccountCount,
"bank for token index {} not found",
token_index
))
}
fn perp_market(
@ -146,25 +214,25 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
}
impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
fn available_banks(&self) -> Result<Vec<TokenIndex>> {
let mut result = Vec::with_capacity(self.n_banks);
for bank_ai in &self.ais[0..self.n_banks] {
let bank = bank_ai.load_fully_unchecked::<Bank>()?;
result.push(bank.token_index);
}
Ok(result)
}
fn bank_and_oracle(
&self,
group: &Pubkey,
active_token_position_index: usize,
token_index: TokenIndex,
) -> Result<(&Bank, I80F48)> {
let bank_account_index = active_token_position_index;
let bank = self
.bank(group, bank_account_index, token_index)
.with_context(|| {
format!(
"loading bank with health account index {}, token index {}, passed account {}",
bank_account_index,
token_index,
self.ais[bank_account_index].key(),
)
})?;
let (bank_account_index, bank) =
self.bank(group, active_token_position_index, token_index)?;
let oracle_index = self.n_banks + active_token_position_index;
let oracle_index = self.n_banks + bank_account_index;
let oracle_acc_infos = &self.create_oracle_infos(oracle_index, &bank.fallback_oracle);
let oracle_price_result = bank.oracle_price(oracle_acc_infos, self.staleness_slot);
let oracle_price = oracle_price_result.with_context(|| {
@ -505,6 +573,10 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
}
impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
fn available_banks(&self) -> Result<Vec<TokenIndex>> {
Ok(self.banks_and_oracles.index_map.keys().copied().collect())
}
fn bank_and_oracle(
&self,
_group: &Pubkey,
@ -530,6 +602,8 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
#[cfg(test)]
mod tests {
use crate::state::{MangoAccount, MangoAccountValue};
use super::super::test::*;
use super::*;
use serum_dex::state::OpenOrders;
@ -650,4 +724,98 @@ mod tests {
.perp_market_and_oracle_price(&group, 1, 5)
.is_err());
}
#[test]
fn test_fixed_account_retriever_with_skips() {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 10, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 20, 2.0, 0.2, 0.1);
let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 30, 3.0, 0.2, 0.1);
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 2.0, 9, (0.2, 0.1), (0.05, 0.02));
let mut oracle2_clone = oracle2.clone();
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account.ensure_token_position(10).unwrap();
account.ensure_token_position(20).unwrap();
account.ensure_token_position(30).unwrap();
account.ensure_perp_position(9, 10).unwrap();
// pass all
{
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
bank3.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
oracle3.as_account_info(),
perp1.as_account_info(),
oracle2_clone.as_account_info(),
];
let retriever =
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
.unwrap();
assert_eq!(retriever.available_banks(), Ok(vec![10, 20, 30]));
let (i, bank) = retriever.bank(&group, 0, 10).unwrap();
assert_eq!(i, 0);
assert_eq!(bank.token_index, 10);
let (i, bank) = retriever.bank(&group, 1, 20).unwrap();
assert_eq!(i, 1);
assert_eq!(bank.token_index, 20);
let (i, bank) = retriever.bank(&group, 2, 30).unwrap();
assert_eq!(i, 2);
assert_eq!(bank.token_index, 30);
assert!(retriever.perp_market(&group, 6, 9).is_ok());
}
// skip bank2
{
let ais = vec![
bank1.as_account_info(),
bank3.as_account_info(),
oracle1.as_account_info(),
oracle3.as_account_info(),
perp1.as_account_info(),
oracle2_clone.as_account_info(),
];
let retriever =
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
.unwrap();
assert_eq!(retriever.available_banks(), Ok(vec![10, 30]));
let (i, bank) = retriever.bank(&group, 0, 10).unwrap();
assert_eq!(i, 0);
assert_eq!(bank.token_index, 10);
let (i, bank) = retriever.bank(&group, 2, 30).unwrap();
assert_eq!(i, 1);
assert_eq!(bank.token_index, 30);
assert!(retriever.bank(&group, 1, 20).is_err());
assert!(retriever.perp_market(&group, 4, 9).is_ok());
}
// skip all
{
let ais = vec![perp1.as_account_info(), oracle2_clone.as_account_info()];
let retriever =
new_fixed_order_account_retriever_with_optional_banks(&ais, &account.borrow(), 0)
.unwrap();
assert_eq!(retriever.available_banks(), Ok(vec![]));
assert!(retriever.bank(&group, 0, 10).is_err());
assert!(retriever.bank(&group, 1, 20).is_err());
assert!(retriever.bank(&group, 2, 30).is_err());
assert!(retriever.perp_market(&group, 0, 9).is_ok());
}
}
}

View File

@ -96,7 +96,7 @@ pub fn compute_health_from_fixed_accounts(
ais: &[AccountInfo],
now_ts: u64,
) -> Result<I80F48> {
let retriever = new_fixed_order_account_retriever(ais, account)?;
let retriever = new_fixed_order_account_retriever(ais, account, Clock::get()?.slot)?;
Ok(new_health_cache(account, &retriever, now_ts)?.health(health_type))
}
@ -820,6 +820,12 @@ impl HealthCache {
})
}
pub fn has_token_info(&self, token_index: TokenIndex) -> bool {
self.token_infos
.iter()
.any(|t| t.token_index == token_index)
}
pub fn perp_info(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpInfo> {
Ok(&self.perp_infos[self.perp_info_index(perp_market_index)?])
}
@ -1234,11 +1240,11 @@ pub fn new_health_cache(
}
/// Generate a special HealthCache for an account and its health accounts
/// where nonnegative token positions for bad oracles are skipped.
/// where nonnegative token positions for bad oracles are skipped as well as missing banks.
///
/// This health cache must be used carefully, since it doesn't provide the actual
/// account health, just a value that is guaranteed to be less than it.
pub fn new_health_cache_skipping_bad_oracles(
pub fn new_health_cache_skipping_missing_banks_and_bad_oracles(
account: &MangoAccountRef,
retriever: &impl AccountRetriever,
now_ts: u64,
@ -1246,22 +1252,49 @@ pub fn new_health_cache_skipping_bad_oracles(
new_health_cache_impl(account, retriever, now_ts, true)
}
// On `allow_skipping_banks`:
// If (a Bank is not provided or its oracle is stale or inconfident) and the health contribution would
// not be negative, skip it. This decreases health, but many operations are still allowed as long
// as the decreased amount stays positive.
fn new_health_cache_impl(
account: &MangoAccountRef,
retriever: &impl AccountRetriever,
now_ts: u64,
// If an oracle is stale or inconfident and the health contribution would
// not be negative, skip it. This decreases health, but maybe overall it's
// still positive?
skip_bad_oracles: bool,
allow_skipping_banks: bool,
) -> Result<HealthCache> {
// token contribution from token accounts
let mut token_infos = Vec::with_capacity(account.active_token_positions().count());
// As a CU optimization, don't call available_banks() unless necessary
let available_banks_opt = if allow_skipping_banks {
Some(retriever.available_banks()?)
} else {
None
};
for (i, position) in account.active_token_positions().enumerate() {
// Allow skipping of missing banks only if the account has a nonnegative balance
if allow_skipping_banks {
let bank_is_available = available_banks_opt
.as_ref()
.unwrap()
.contains(&position.token_index);
if !bank_is_available {
require_msg_typed!(
position.indexed_position >= 0,
MangoError::InvalidBank,
"the bank for token index {} is a required health account when the account has a negative balance in it",
position.token_index
);
continue;
}
}
let bank_oracle_result =
retriever.bank_and_oracle(&account.fixed.group, i, position.token_index);
if skip_bad_oracles
// Allow skipping of bad-oracle banks if the account has a nonnegative balance
if allow_skipping_banks
&& bank_oracle_result.is_oracle_error()
&& position.indexed_position >= 0
{
@ -1301,9 +1334,25 @@ fn new_health_cache_impl(
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
// find the TokenInfos for the market's base and quote tokens
let base_info_index = find_token_info_index(&token_infos, serum_account.base_token_index)?;
let quote_info_index =
find_token_info_index(&token_infos, serum_account.quote_token_index)?;
// and potentially skip the whole serum contribution if they are not available
let info_index_results = (
find_token_info_index(&token_infos, serum_account.base_token_index),
find_token_info_index(&token_infos, serum_account.quote_token_index),
);
let (base_info_index, quote_info_index) = match info_index_results {
(Ok(base), Ok(quote)) => (base, quote),
_ => {
require_msg_typed!(
allow_skipping_banks,
MangoError::InvalidBank,
"serum market {} misses health accounts for bank {} or {}",
serum_account.market_index,
serum_account.base_token_index,
serum_account.quote_token_index,
);
continue;
}
};
// add the amounts that are freely settleable immediately to token balances
let base_free = I80F48::from(oo.native_coin_free);
@ -1329,6 +1378,12 @@ fn new_health_cache_impl(
i,
perp_position.market_index,
)?;
// Ensure the settle token is available in the health cache
if allow_skipping_banks {
find_token_info_index(&token_infos, perp_market.settle_token_index)?;
}
perp_infos.push(PerpInfo::new(
perp_position,
perp_market,
@ -1879,4 +1934,170 @@ mod tests {
test_health1_runner(testcase);
}
}
#[test]
fn test_health_with_skips() {
let testcase = TestHealth1Case {
// 6, reserved oo funds
token1: 100,
token2: 10,
token3: -10,
oo_1_2: (5, 1),
oo_1_3: (0, 0),
..Default::default()
};
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
let group = Pubkey::new_unique();
account.fixed.group = group;
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3);
bank1
.data()
.change_without_fee(
account.ensure_token_position(0).unwrap().0,
I80F48::from(testcase.token1),
DUMMY_NOW_TS,
)
.unwrap();
bank2
.data()
.change_without_fee(
account.ensure_token_position(4).unwrap().0,
I80F48::from(testcase.token2),
DUMMY_NOW_TS,
)
.unwrap();
bank3
.data()
.change_without_fee(
account.ensure_token_position(5).unwrap().0,
I80F48::from(testcase.token3),
DUMMY_NOW_TS,
)
.unwrap();
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account1 = account.create_serum3_orders(2).unwrap();
serum3account1.open_orders = oo1.pubkey;
serum3account1.base_token_index = 4;
serum3account1.quote_token_index = 0;
oo1.data().native_pc_total = testcase.oo_1_2.0;
oo1.data().native_coin_total = testcase.oo_1_2.1;
fn compute_health_with_retriever<'a, 'info>(
ais: &[AccountInfo],
account: &MangoAccountValue,
group: Pubkey,
kind: bool,
) -> Result<I80F48> {
let hc = if kind {
let retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
DUMMY_NOW_TS,
)?
} else {
let retriever = new_fixed_order_account_retriever_with_optional_banks(
&ais,
&account.borrow(),
0,
)
.unwrap();
new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
DUMMY_NOW_TS,
)?
};
Ok(hc.health(HealthType::Init))
}
for retriever_kind in [false, true] {
// baseline with everything
{
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
bank3.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
oracle3.as_account_info(),
oo1.as_account_info(),
];
let health =
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
assert!(health_eq(
health,
0.8 * 100.0 + 0.5 * 5.0 * (10.0 + 2.0) - 1.5 * 10.0 * 10.0
));
}
// missing bank1
{
let ais = vec![
bank2.as_account_info(),
bank3.as_account_info(),
oracle2.as_account_info(),
oracle3.as_account_info(),
oo1.as_account_info(),
];
let health =
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
assert!(health_eq(health, 0.5 * 5.0 * 10.0 - 1.5 * 10.0 * 10.0));
}
// missing bank2
{
let ais = vec![
bank1.as_account_info(),
bank3.as_account_info(),
oracle1.as_account_info(),
oracle3.as_account_info(),
oo1.as_account_info(),
];
let health =
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
assert!(health_eq(health, 0.8 * 100.0 - 1.5 * 10.0 * 10.0));
}
// missing bank1 and 2
{
let ais = vec![
bank3.as_account_info(),
oracle3.as_account_info(),
oo1.as_account_info(),
];
let health =
compute_health_with_retriever(&ais, &account, group, retriever_kind).unwrap();
assert!(health_eq(health, -1.5 * 10.0 * 10.0));
}
// missing bank3
{
let ais = vec![
bank1.as_account_info(),
bank2.as_account_info(),
oracle1.as_account_info(),
oracle2.as_account_info(),
oo1.as_account_info(),
];
// bank3 has a negative balance and can't be skipped!
assert!(
compute_health_with_retriever(&ais, &account, group, retriever_kind).is_err()
);
}
}
}
}

View File

@ -2,9 +2,10 @@ use crate::accounts_ix::*;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::group_seeds;
use crate::health::{new_fixed_order_account_retriever, new_health_cache, AccountRetriever};
use crate::health::*;
use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog};
use crate::state::*;
use crate::util::clock_now;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
@ -368,8 +369,9 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
// all vaults must have had matching banks
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
require_msg!(
require_msg_typed!(
has_bank,
MangoError::InvalidBank,
"missing bank for vault index {}, address {}",
i,
vaults[i].key
@ -387,12 +389,26 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
}
// Check health before balance adjustments
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
// The vault-to-bank matching above ensures that the banks for the affected tokens are available.
let (now_ts, now_slot) = clock_now();
let retriever = new_fixed_order_account_retriever_with_optional_banks(
health_ais,
&account.borrow(),
now_slot,
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)?;
let pre_init_health = account.check_health_pre(&health_cache)?;
// Prices for logging and net borrow checks
//
// This also verifies that all affected banks/oracles are available in health_cache:
// That is essential to avoid issues around withdrawing tokens when init health is negative
// (similar issue to token_withdraw)
let mut oracle_prices = vec![];
for change in &changes {
let (_, oracle_price) = retriever.bank_and_oracle(
@ -400,6 +416,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
change.bank_index,
change.token_index,
)?;
// Sanity check
health_cache.token_info_index(change.token_index)?;
oracle_prices.push(oracle_price);
}
@ -502,8 +520,16 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
});
// Check health after account position changes
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
let retriever = new_fixed_order_account_retriever_with_optional_banks(
health_ais,
&account.borrow(),
now_slot,
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)?;
account.check_health_post(&health_cache, pre_init_health)?;
// Deactivate inactive token accounts after health check

View File

@ -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(())
}

View File

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

View File

@ -16,6 +16,7 @@ pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use group_withdraw_insurance_fund::*;
pub use health_check::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use perp_cancel_all_orders::*;
@ -35,6 +36,7 @@ pub use perp_place_order::*;
pub use perp_settle_fees::*;
pub use perp_settle_pnl::*;
pub use perp_update_funding::*;
pub use sequence_check::*;
pub use serum3_cancel_all_orders::*;
pub use serum3_cancel_order::*;
pub use serum3_cancel_order_by_client_order_id::*;
@ -85,6 +87,7 @@ mod group_close;
mod group_create;
mod group_edit;
mod group_withdraw_insurance_fund;
mod health_check;
mod health_region;
mod ix_gate_set;
mod perp_cancel_all_orders;
@ -104,6 +107,7 @@ mod perp_place_order;
mod perp_settle_fees;
mod perp_settle_pnl;
mod perp_update_funding;
mod sequence_check;
mod serum3_cancel_all_orders;
mod serum3_cancel_order;
mod serum3_cancel_order_by_client_order_id;

View File

@ -4,6 +4,7 @@ use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::state::*;
use crate::util::clock_now;
pub fn perp_liq_force_cancel_orders(
ctx: Context<PerpLiqForceCancelOrders>,
@ -11,10 +12,10 @@ pub fn perp_liq_force_cancel_orders(
) -> Result<()> {
let mut account = ctx.accounts.account.load_full_mut()?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let (now_ts, now_slot) = clock_now();
let mut health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
new_health_cache(&account.borrow(), &retriever, now_ts).context("create health cache")?
};

View File

@ -3,8 +3,9 @@ use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::health::{new_fixed_order_account_retriever, new_health_cache};
use crate::health::*;
use crate::state::*;
use crate::util::clock_now;
// TODO
#[allow(clippy::too_many_arguments)]
@ -16,7 +17,7 @@ pub fn perp_place_order(
require_gte!(order.max_base_lots, 0);
require_gte!(order.max_quote_lots, 0);
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let (now_ts, now_slot) = clock_now();
let oracle_price;
// Update funding if possible.
@ -66,10 +67,21 @@ pub fn perp_place_order(
// Pre-health computation, _after_ perp position is created
//
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
.context("pre-withdraw init health")?;
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)
.context("pre init health")?;
// The settle token banks/oracles must be passed and be valid
health_cache.token_info_index(settle_token_index)?;
let pre_init_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_init_health))
} else {

View File

@ -9,6 +9,7 @@ use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog, TokenBalanceLog};
use crate::util::clock_now;
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
// max_settle_amount must greater than zero
@ -123,8 +124,9 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
drop(perp_market);
// Verify that the result of settling did not violate the health of the account that lost money
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let (now_ts, now_slot) = clock_now();
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever, now_ts)?;
require!(health >= 0, MangoError::HealthMustBePositive);

View File

@ -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(())
}

View File

@ -8,6 +8,7 @@ use crate::instructions::charge_loan_origination_fees;
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::*;
use crate::util::clock_now;
pub fn serum3_liq_force_cancel_orders(
ctx: Context<Serum3LiqForceCancelOrders>,
@ -50,14 +51,15 @@ pub fn serum3_liq_force_cancel_orders(
);
}
let (now_ts, now_slot) = clock_now();
//
// Early return if if liquidation is not allowed or if market is not in force close
//
let mut health_cache = {
let mut account = ctx.accounts.account.load_full_mut()?;
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
.context("create health cache")?;

View File

@ -9,6 +9,7 @@ use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
use crate::serum3_cpi::{
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
};
use crate::util::clock_now;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
@ -40,6 +41,7 @@ pub fn serum3_place_order(
// Validation
//
let receiver_token_index;
let payer_token_index;
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
@ -60,7 +62,7 @@ pub fn serum3_place_order(
// Validate bank and vault #3
let payer_bank = ctx.accounts.payer_bank.load()?;
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
let payer_token_index = match side {
payer_token_index = match side {
Serum3Side::Bid => serum_market.quote_token_index,
Serum3Side::Ask => serum_market.base_token_index,
};
@ -76,10 +78,23 @@ pub fn serum3_place_order(
// Pre-health computation
//
let mut account = ctx.accounts.account.load_full_mut()?;
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let mut health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
.context("pre-withdraw init health")?;
let (now_ts, now_slot) = clock_now();
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
)?;
let mut health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)
.context("pre init health")?;
// The payer and receiver token banks/oracles must be passed and be valid
health_cache.token_info_index(payer_token_index)?;
health_cache.token_info_index(receiver_token_index)?;
let pre_health_opt = if !account.fixed.is_in_health_region() {
let pre_init_health = account.check_health_pre(&health_cache)?;
Some(pre_init_health)
@ -412,6 +427,20 @@ pub fn serum3_place_order(
// Note that all orders on the book executing can still cause a net deposit. That's because
// the total serum3 potential amount assumes all reserved amounts convert at the current
// oracle price.
//
// This also requires that all serum3 oos that touch the receiver_token are avaliable in the
// health cache. We make this a general requirement to avoid surprises.
for serum3 in account.active_serum3_orders() {
if serum3.base_token_index == receiver_token_index
|| serum3.quote_token_index == receiver_token_index
{
require_msg!(
health_cache.serum3_infos.iter().any(|s3| s3.market_index == serum3.market_index),
"health cache is missing serum3 info {} involving receiver token {}; passed banks and oracles?",
serum3.market_index, receiver_token_index
);
}
}
if receiver_bank_reduce_only {
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
let potential =

View File

@ -1,6 +1,7 @@
use crate::accounts_zerocopy::*;
use crate::health::*;
use crate::state::*;
use crate::util::clock_now;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
@ -10,7 +11,7 @@ use crate::logs::{emit_stack, TokenBalanceLog, TokenCollateralFeeLog};
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
let group = ctx.accounts.group.load()?;
let mut account = ctx.accounts.account.load_full_mut()?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let (now_ts, now_slot) = clock_now();
if group.collateral_fee_interval == 0 {
// By resetting, a new enabling of collateral fees will not immediately create a charge
@ -42,7 +43,7 @@ pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) ->
let health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
new_health_cache(&account.borrow(), &retriever, now_ts)?
};

View File

@ -11,6 +11,7 @@ use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::*;
use crate::util::clock_now;
struct DepositCommon<'a, 'info> {
pub group: &'a AccountLoader<'info, Group>,
@ -119,13 +120,21 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
//
// Health computation
//
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let (now_ts, now_slot) = clock_now();
let retriever = new_fixed_order_account_retriever_with_optional_banks(
remaining_accounts,
&account.borrow(),
now_slot,
)?;
// We only compute health to check if the account leaves the being_liquidated state.
// So it's ok to possibly skip token positions for bad oracles and compute a health
// So it's ok to possibly skip nonnegative token positions and compute a health
// value that is too low.
let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)?;
let cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)?;
// Since depositing can only increase health, we can skip the usual pre-health computation.
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
@ -143,6 +152,13 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
// Group level deposit limit on account
let group = self.group.load()?;
if group.deposit_limit_quote > 0 {
// Requires that all banks were provided an all oracles are healthy, otherwise we
// can't know how much this account has deposited
require_eq!(
cache.token_infos.len(),
account.active_token_positions().count()
);
let assets = cache
.health_assets_and_liabs_stable_assets(HealthType::Init)
.0

View File

@ -2,6 +2,7 @@ use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::health::*;
use crate::state::*;
use crate::util::clock_now;
use anchor_lang::prelude::*;
use anchor_spl::associated_token;
use anchor_spl::token;
@ -19,7 +20,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let group = ctx.accounts.group.load()?;
let token_index = ctx.accounts.bank.load()?.token_index;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let (now_ts, now_slot) = clock_now();
// Create the account's position for that token index
let mut account = ctx.accounts.account.load_full_mut()?;
@ -27,21 +28,19 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// Health check _after_ the token position is guaranteed to exist
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let hc_result = new_health_cache(&account.borrow(), &retriever, now_ts)
.context("pre-withdraw health cache");
if hc_result.is_oracle_error() {
// We allow NOT checking the pre init health. That means later on the health
// check will be stricter (post_init > 0, without the post_init >= pre_init option)
// Then later we can compute the health while ignoring potential nonnegative
// health contributions from tokens with stale oracles.
None
} else {
let health_cache = hc_result?;
let pre_init_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_init_health))
}
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)
.context("pre-withdraw health cache")?;
let pre_init_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_init_health))
} else {
None
};
@ -156,26 +155,29 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
//
// Health check
//
if !account.fixed.is_in_health_region() {
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
// This is the normal case
if let Some((mut health_cache, pre_init_health_lower_bound)) = pre_health_opt {
if health_cache.has_token_info(token_index) {
// This is the normal case: the health cache knows about the token, we can
// compute the health for the new state by adjusting its balance
health_cache.adjust_token_balance(&bank, native_position_after - native_position)?;
account.check_health_post(&health_cache, pre_init_health)?;
account.check_health_post(&health_cache, pre_init_health_lower_bound)?;
} else {
// Some oracle was stale/not confident enough above.
// The health cache does not know about the token! It has a bad oracle or wasn't
// provided in the health accounts. Borrows are out of the question!
require!(!is_borrow, MangoError::BorrowsRequireHealthAccountBank);
// Since the health cache isn't aware of the bank we changed, the health
// estimation is the same.
let post_init_health_lower_bound = pre_init_health_lower_bound;
// If health without the token is positive, then full health is positive and
// withdrawing all of the token would still keep it positive.
// However, if health without it is negative then full health could be negative
// and could be made worse by withdrawals.
//
// Try computing health while ignoring nonnegative contributions from bad oracles.
// If the health is good enough without those, we can pass.
//
// Note that this must include the normal pre and post health checks.
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)
.context("special post-withdraw health-cache")?;
let post_init_health = health_cache.health(HealthType::Init);
account.check_health_pre_checks(&health_cache, post_init_health)?;
account.check_health_post_checks(I80F48::MAX, post_init_health)?;
// We don't know the true pre_init_health: So require that our lower bound on
// post health is strictly good enough.
account.check_health_post_checks_strict(post_init_health_lower_bound)?;
}
}

View File

@ -458,6 +458,22 @@ pub mod mango_v4 {
Ok(())
}
pub fn sequence_check(ctx: Context<SequenceCheck>, expected_sequence_number: u8) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::sequence_check(ctx, expected_sequence_number)?;
Ok(())
}
pub fn health_check(
ctx: Context<HealthCheck>,
min_health_value: f64,
check_kind: HealthCheckKind,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::health_check(ctx, min_health_value, check_kind)?;
Ok(())
}
// todo:
// ckamm: generally, using an I80F48 arg will make it harder to call
// because generic anchor clients won't know how to deal with it

View File

@ -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,
})
}

View File

@ -246,6 +246,8 @@ pub enum IxGate {
TokenConditionalSwapCreateLinearAuction = 70,
Serum3PlaceOrderV2 = 71,
TokenForceWithdraw = 72,
SequenceCheck = 73,
HealthCheck = 74,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -123,8 +123,7 @@ pub struct MangoAccount {
pub bump: u8,
#[derivative(Debug = "ignore")]
pub padding: [u8; 1],
pub sequence_number: u8,
// (Display only)
// Cumulative (deposits - withdraws)
@ -200,7 +199,7 @@ impl MangoAccount {
in_health_region: 0,
account_num: 0,
bump: 0,
padding: Default::default(),
sequence_number: 0,
net_deposits: 0,
perp_spot_transfers: 0,
health_region_begin_init_health: 0,
@ -325,7 +324,7 @@ pub struct MangoAccountFixed {
being_liquidated: u8,
in_health_region: u8,
pub bump: u8,
pub padding: [u8; 1],
pub sequence_number: u8,
pub net_deposits: i64,
pub perp_spot_transfers: i64,
pub health_region_begin_init_health: i64,
@ -1458,6 +1457,13 @@ impl<
Ok(())
}
/// A stricter version of check_health_post_checks() that requires >=0 health, it not getting
/// worse is not sufficient
pub fn check_health_post_checks_strict(&mut self, post_init_health: I80F48) -> Result<()> {
require!(post_init_health >= 0, MangoError::HealthMustBePositive);
Ok(())
}
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
// Once maint_health falls below 0, we want to start liquidating,
// we want to allow liquidation to continue until init_health is positive,
@ -2897,7 +2903,7 @@ mod tests {
being_liquidated: fixed.being_liquidated,
in_health_region: fixed.in_health_region,
bump: fixed.bump,
padding: Default::default(),
sequence_number: 0,
net_deposits: fixed.net_deposits,
perp_spot_transfers: fixed.perp_spot_transfers,
health_region_begin_init_health: fixed.health_region_begin_init_health,

View File

@ -1,3 +1,4 @@
pub use amm_cpi::*;
pub use bank::*;
pub use dynamic_account::*;
pub use equity::*;
@ -7,13 +8,13 @@ pub use mango_account_components::*;
pub use mint_info::*;
pub use openbook_v2_market::*;
pub use oracle::*;
pub use orca_cpi::*;
pub use orderbook::*;
pub use perp_market::*;
pub use serum3_market::*;
pub use stable_price::*;
pub use token_conditional_swap::*;
mod amm_cpi;
mod bank;
mod dynamic_account;
mod equity;
@ -23,7 +24,6 @@ mod mango_account_components;
mod mint_info;
mod openbook_v2_market;
mod oracle;
mod orca_cpi;
mod orderbook;
mod perp_market;
mod serum3_market;

View File

@ -3,7 +3,7 @@ use std::mem::size_of;
use anchor_lang::prelude::*;
use anchor_lang::{AnchorDeserialize, Discriminator};
use derivative::Derivative;
use fixed::types::{I80F48, U64F64};
use fixed::types::I80F48;
use static_assertions::const_assert_eq;
use switchboard_program::FastRoundResultAccountData;
@ -12,9 +12,9 @@ use switchboard_v2::AggregatorAccountData;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::load_whirlpool_state;
use crate::state::load_orca_pool_state;
use super::orca_mainnet_whirlpool;
use super::{load_raydium_pool_state, orca_mainnet_whirlpool, raydium_mainnet};
const DECIMAL_CONSTANT_ZERO_INDEX: i8 = 12;
const DECIMAL_CONSTANTS: [I80F48; 25] = [
@ -117,6 +117,7 @@ pub enum OracleType {
SwitchboardV1,
SwitchboardV2,
OrcaCLMM,
RaydiumCLMM,
}
pub struct OracleState {
@ -195,6 +196,8 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
return Ok(OracleType::SwitchboardV1);
} else if acc_info.owner() == &orca_mainnet_whirlpool::ID {
return Ok(OracleType::OrcaCLMM);
} else if acc_info.owner() == &raydium_mainnet::ID {
return Ok(OracleType::RaydiumCLMM);
}
Err(MangoError::UnknownOracleType.into())
@ -205,18 +208,19 @@ pub fn check_is_valid_fallback_oracle(acc_info: &impl KeyedAccountReader) -> Res
return Ok(());
};
let oracle_type = determine_oracle_type(acc_info)?;
if oracle_type == OracleType::OrcaCLMM {
let whirlpool = load_whirlpool_state(acc_info)?;
let valid_oracle = match oracle_type {
OracleType::OrcaCLMM => {
let whirlpool = load_orca_pool_state(acc_info)?;
whirlpool.has_quote_token()
}
OracleType::RaydiumCLMM => {
let pool = load_raydium_pool_state(acc_info)?;
pool.has_quote_token()
}
_ => true,
};
let has_usdc_token = whirlpool.token_mint_a == usdc_mint_mainnet::ID
|| whirlpool.token_mint_b == usdc_mint_mainnet::ID;
let has_sol_token = whirlpool.token_mint_a == sol_mint_mainnet::ID
|| whirlpool.token_mint_b == sol_mint_mainnet::ID;
require!(
has_usdc_token || has_sol_token,
MangoError::InvalidCLMMOracle
);
}
require!(valid_oracle, MangoError::UnexpectedOracle);
Ok(())
}
@ -253,7 +257,7 @@ fn pyth_get_price(
}
}
fn get_pyth_state(
pub fn get_pyth_state(
acc_info: &(impl KeyedAccountReader + ?Sized),
base_decimals: u8,
) -> Result<OracleState> {
@ -404,56 +408,32 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
}
}
OracleType::OrcaCLMM => {
let whirlpool = load_whirlpool_state(oracle_info)?;
let inverted = whirlpool.is_inverted();
let quote_state = if inverted {
quote_state_unchecked(acc_infos, &whirlpool.token_mint_a)?
} else {
quote_state_unchecked(acc_infos, &whirlpool.token_mint_b)?
};
let clmm_price = if inverted {
let sqrt_price = U64F64::from_bits(whirlpool.sqrt_price).to_num::<f64>();
let inverted_price = sqrt_price * sqrt_price;
I80F48::from_num(1.0f64 / inverted_price)
} else {
let sqrt_price = U64F64::from_bits(whirlpool.sqrt_price);
I80F48::from_num(sqrt_price * sqrt_price)
};
let price = clmm_price * quote_state.price;
let whirlpool = load_orca_pool_state(oracle_info)?;
let clmm_price = whirlpool.get_clmm_price();
let quote_oracle_state = whirlpool.quote_state_unchecked(acc_infos)?;
let price = clmm_price * quote_oracle_state.price;
OracleState {
price,
last_update_slot: quote_state.last_update_slot,
deviation: quote_state.deviation,
last_update_slot: quote_oracle_state.last_update_slot,
deviation: quote_oracle_state.deviation,
oracle_type: OracleType::OrcaCLMM,
}
}
OracleType::RaydiumCLMM => {
let whirlpool = load_raydium_pool_state(oracle_info)?;
let clmm_price = whirlpool.get_clmm_price();
let quote_oracle_state = whirlpool.quote_state_unchecked(acc_infos)?;
let price = clmm_price * quote_oracle_state.price;
OracleState {
price,
last_update_slot: quote_oracle_state.last_update_slot,
deviation: quote_oracle_state.deviation,
oracle_type: OracleType::RaydiumCLMM,
}
}
})
}
fn quote_state_unchecked<T: KeyedAccountReader>(
acc_infos: &OracleAccountInfos<T>,
quote_mint: &Pubkey,
) -> Result<OracleState> {
if quote_mint == &usdc_mint_mainnet::ID {
let usd_feed = acc_infos
.usdc_opt
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
let usd_state = get_pyth_state(usd_feed, QUOTE_DECIMALS as u8)?;
return Ok(usd_state);
} else if quote_mint == &sol_mint_mainnet::ID {
let sol_feed = acc_infos
.sol_opt
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
let sol_state = get_pyth_state(sol_feed, SOL_DECIMALS as u8)?;
return Ok(sol_state);
} else {
return Err(MangoError::MissingFeedForCLMMOracle.into());
}
}
pub fn oracle_log_context(
name: &str,
state: &OracleState,
@ -545,7 +525,87 @@ mod tests {
}
#[test]
pub fn test_clmm_price() -> Result<()> {
pub fn test_clmm_prices() -> Result<()> {
// add ability to find fixtures
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
let usdc_fixture = (
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
OracleType::Pyth,
Pubkey::default(),
6,
);
let clmm_fixtures = vec![
(
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
OracleType::OrcaCLMM,
orca_mainnet_whirlpool::ID,
9, // SOL/USDC pool
),
(
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
OracleType::RaydiumCLMM,
raydium_mainnet::ID,
9, // SOL/USDC pool
),
];
for fixture in clmm_fixtures {
let clmm_file = format!("resources/test/{}.bin", fixture.0);
let mut clmm_data = read_file(find_file(&clmm_file).unwrap());
let data = RefCell::new(&mut clmm_data[..]);
let ai = &AccountInfoRef {
key: &Pubkey::from_str(fixture.0).unwrap(),
owner: &fixture.2,
data: data.borrow(),
};
let pyth_file = format!("resources/test/{}.bin", usdc_fixture.0);
let mut pyth_data = read_file(find_file(&pyth_file).unwrap());
let pyth_data_cell = RefCell::new(&mut pyth_data[..]);
let usdc_ai = &AccountInfoRef {
key: &Pubkey::from_str(usdc_fixture.0).unwrap(),
owner: &usdc_fixture.2,
data: pyth_data_cell.borrow(),
};
let base_decimals = fixture.3;
let usdc_decimals = usdc_fixture.3;
let usdc_ais = OracleAccountInfos {
oracle: usdc_ai,
fallback_opt: None,
usdc_opt: None,
sol_opt: None,
};
let clmm_ais = OracleAccountInfos {
oracle: ai,
fallback_opt: None,
usdc_opt: Some(usdc_ai),
sol_opt: None,
};
let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap();
let clmm = oracle_state_unchecked(&clmm_ais, base_decimals).unwrap();
assert!(usdc.price == I80F48::from_num(1.00000758274099));
match fixture.1 {
OracleType::OrcaCLMM => {
// 63.006792786538313 * 1.00000758274099 (but in native/native)
assert!(clmm.price == I80F48::from_num(0.06300727055072872))
}
OracleType::RaydiumCLMM => {
// 83.551469620431 * 1.00000758274099 (but in native/native)
assert!(clmm.price == I80F48::from_num(0.083552103169584))
}
_ => unimplemented!(),
}
}
Ok(())
}
#[test]
pub fn test_clmm_price_missing_usdc() -> Result<()> {
// add ability to find fixtures
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
@ -558,67 +618,13 @@ mod tests {
9, // SOL/USDC pool
),
(
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
OracleType::Pyth,
Pubkey::default(),
6,
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
OracleType::RaydiumCLMM,
raydium_mainnet::ID,
9, // SOL/USDC pool
),
];
let clmm_file = format!("resources/test/{}.bin", fixtures[0].0);
let mut clmm_data = read_file(find_file(&clmm_file).unwrap());
let data = RefCell::new(&mut clmm_data[..]);
let ai = &AccountInfoRef {
key: &Pubkey::from_str(fixtures[0].0).unwrap(),
owner: &fixtures[0].2,
data: data.borrow(),
};
let pyth_file = format!("resources/test/{}.bin", fixtures[1].0);
let mut pyth_data = read_file(find_file(&pyth_file).unwrap());
let pyth_data_cell = RefCell::new(&mut pyth_data[..]);
let usdc_ai = &AccountInfoRef {
key: &Pubkey::from_str(fixtures[1].0).unwrap(),
owner: &fixtures[1].2,
data: pyth_data_cell.borrow(),
};
let base_decimals = fixtures[0].3;
let usdc_decimals = fixtures[1].3;
let usdc_ais = OracleAccountInfos {
oracle: usdc_ai,
fallback_opt: None,
usdc_opt: None,
sol_opt: None,
};
let orca_ais = OracleAccountInfos {
oracle: ai,
fallback_opt: None,
usdc_opt: Some(usdc_ai),
sol_opt: None,
};
let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap();
let orca = oracle_state_unchecked(&orca_ais, base_decimals).unwrap();
assert!(usdc.price == I80F48::from_num(1.00000758274099));
// 63.006792786538313 * 1.00000758274099 (but in native/native)
assert!(orca.price == I80F48::from_num(0.06300727055072872));
Ok(())
}
#[test]
pub fn test_clmm_price_missing_usdc() -> Result<()> {
// add ability to find fixtures
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
let fixtures = vec![(
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
OracleType::OrcaCLMM,
orca_mainnet_whirlpool::ID,
9, // SOL/USDC pool
)];
for fixture in fixtures {
let filename = format!("resources/test/{}.bin", fixture.0);
let mut clmm_data = read_file(find_file(&filename).unwrap());
@ -642,4 +648,47 @@ mod tests {
Ok(())
}
#[test]
pub fn test_valid_fallbacks() -> Result<()> {
// add ability to find fixtures
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
let usdc_fixture = (
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
OracleType::Pyth,
Pubkey::default(),
6,
);
let clmm_fixtures = vec![
(
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
OracleType::OrcaCLMM,
orca_mainnet_whirlpool::ID,
9, // SOL/USDC pool
),
(
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
OracleType::RaydiumCLMM,
raydium_mainnet::ID,
9, // SOL/USDC pool
),
];
for fixture in clmm_fixtures {
let clmm_file = format!("resources/test/{}.bin", fixture.0);
let mut clmm_data = read_file(find_file(&clmm_file).unwrap());
let data = RefCell::new(&mut clmm_data[..]);
let ai = &AccountInfoRef {
key: &Pubkey::from_str(fixture.0).unwrap(),
owner: &fixture.2,
data: data.borrow(),
};
check_is_valid_fallback_oracle(ai)?;
}
Ok(())
}
}

View File

@ -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,
})
}

View File

@ -31,6 +31,12 @@ pub fn format_zero_terminated_utf8_bytes(
)
}
// Returns (now_ts, now_slot)
pub fn clock_now() -> (u64, u64) {
let clock = Clock::get().unwrap();
(clock.unix_timestamp.try_into().unwrap(), clock.slot)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -10,7 +10,8 @@ pub use program_test::*;
pub use super::program_test;
pub use utils::assert_equal_fixed_f64 as assert_equal;
pub use crate::assert_eq_f64;
pub use crate::assert_eq_fixed_f64;
mod test_alt;
mod test_bankrupt_tokens;
@ -21,6 +22,7 @@ mod test_collateral_fees;
mod test_delegate;
mod test_fees_buyback_with_mngo;
mod test_force_close;
mod test_health_check;
mod test_health_compute;
mod test_health_region;
mod test_ix_gate_set;
@ -35,6 +37,7 @@ mod test_perp_settle;
mod test_perp_settle_fees;
mod test_position_lifetime;
mod test_reduce_only;
mod test_replay;
mod test_serum;
mod test_stale_oracles;
mod test_token_conditional_swap;

View File

@ -450,7 +450,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
.await;
let maint_health = account_maint_health(solana, account).await;
assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2));
assert_eq_f64!(maint_health, 1000.0, 1e-2);
let start_time = solana.clock_timestamp().await;
@ -476,17 +476,17 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
.unwrap();
let maint_health = account_maint_health(solana, account).await;
assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2));
assert_eq_f64!(maint_health, 1000.0, 1e-2);
solana.set_clock_timestamp(start_time + 1500).await;
let maint_health = account_maint_health(solana, account).await;
assert!(assert_equal_f64_f64(maint_health, 750.0, 1e-2));
assert_eq_f64!(maint_health, 750.0, 1e-2);
solana.set_clock_timestamp(start_time + 3000).await;
let maint_health = account_maint_health(solana, account).await;
assert!(assert_equal_f64_f64(maint_health, 500.0, 1e-2));
assert_eq_f64!(maint_health, 500.0, 1e-2);
solana.set_clock_timestamp(start_time + 1600).await;
@ -507,11 +507,11 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
.unwrap();
let maint_health = account_maint_health(solana, account).await;
assert!(assert_equal_f64_f64(maint_health, 700.0, 1e-2));
assert_eq_f64!(maint_health, 700.0, 1e-2);
let bank: Bank = solana.get_account(tokens[0].bank).await;
assert!(assert_equal_fixed_f64(bank.maint_asset_weight, 0.7, 1e-4));
assert!(assert_equal_fixed_f64(bank.maint_liab_weight, 1.3, 1e-4));
assert_eq_fixed_f64!(bank.maint_asset_weight, 0.7, 1e-4);
assert_eq_fixed_f64!(bank.maint_liab_weight, 1.3, 1e-4);
assert_eq!(bank.maint_weight_shift_duration_inv, I80F48::ZERO);
Ok(())
@ -687,3 +687,365 @@ async fn test_bank_deposit_limit() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_withdraw_skip_bank() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let payer_token_accounts = &context.users[1].token_accounts;
let mints = &context.mints[0..3];
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
zero_token_is_quote: true,
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;
// Funding to fill the vaults
create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints,
1_000_000,
0,
)
.await;
let account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..2],
1000,
0,
)
.await;
//
// TEST: when all balances are positive
//
send_tx(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank],
},
)
.await
.unwrap();
send_tx(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[1].bank],
},
)
.await
.unwrap();
// ok even when total health = 0
send_tx(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank, tokens[1].bank],
},
)
.await
.unwrap();
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1001,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank],
},
MangoError::BorrowsRequireHealthAccountBank
);
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1001,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[1].bank],
},
MangoError::HealthMustBePositiveOrIncrease
);
//
// TEST: create a borrow
//
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[2],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank, tokens[1].bank],
},
MangoError::HealthMustBePositiveOrIncrease
);
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[2],
bank_index: 0,
},
skip_banks: vec![tokens[2].bank],
},
MangoError::BorrowsRequireHealthAccountBank
);
send_tx(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[2],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank],
},
)
.await
.unwrap();
//
// TEST: withdraw positive balances when there's a borrow
//
send_tx(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank],
},
)
.await
.unwrap();
send_tx(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[1].bank],
},
)
.await
.unwrap();
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[2].bank],
},
MangoError::InvalidBank
);
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account,
owner,
token_account: payer_token_accounts[0],
bank_index: 0,
},
skip_banks: vec![tokens[0].bank, tokens[1].bank],
},
MangoError::HealthMustBePositive
);
Ok(())
}
#[tokio::test]
async fn test_sequence_check() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..1];
let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 6,
serum3_count: 3,
perp_count: 3,
perp_oo_count: 3,
token_conditional_swap_count: 3,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let mango_account = get_mango_account(solana, account).await;
assert_eq!(mango_account.fixed.sequence_number, 0);
//
// TEST: Sequence check with right sequence number
//
send_tx(
solana,
SequenceCheckInstruction {
account,
owner,
expected_sequence_number: 0,
},
)
.await
.unwrap();
let mango_account = get_mango_account(solana, account).await;
assert_eq!(mango_account.fixed.sequence_number, 1);
send_tx(
solana,
SequenceCheckInstruction {
account,
owner,
expected_sequence_number: 1,
},
)
.await
.unwrap();
let mango_account = get_mango_account(solana, account).await;
assert_eq!(mango_account.fixed.sequence_number, 2);
//
// TEST: Sequence check with wrong sequence number
//
send_tx_expect_error!(
solana,
SequenceCheckInstruction {
account,
owner,
expected_sequence_number: 1
},
MangoError::InvalidSequenceNumber
);
send_tx_expect_error!(
solana,
SequenceCheckInstruction {
account,
owner,
expected_sequence_number: 4
},
MangoError::InvalidSequenceNumber
);
let mango_account = get_mango_account(solana, account).await;
assert_eq!(mango_account.fixed.sequence_number, 2);
Ok(())
}

View File

@ -221,7 +221,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
// fails because borrow is greater than remaining margin in net borrow limit
// (requires the test to be quick enough to avoid accidentally going to the next borrow limit window!)
let res = send_tx(
send_tx_expect_error!(
solana,
TokenWithdrawInstruction {
amount: 4000,
@ -231,12 +231,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await;
assert_mango_error(
&res,
MangoError::BankNetBorrowsLimitReached.into(),
"".into(),
MangoError::BankNetBorrowsLimitReached
);
// succeeds because is not a borrow
@ -314,7 +309,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 10.0).await;
// cannot borrow anything: net borrowed 1002 * price 10.0 > limit 6000
let res = send_tx(
send_tx_expect_error!(
solana,
TokenWithdrawInstruction {
amount: 1,
@ -324,12 +319,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await;
assert_mango_error(
&res,
MangoError::BankNetBorrowsLimitReached.into(),
"".into(),
MangoError::BankNetBorrowsLimitReached
);
// can still withdraw
@ -350,7 +340,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 5.0).await;
// cannot borrow this much: (net borrowed 1000 + new borrow 201) * price 5.0 > limit 6000
let res = send_tx(
send_tx_expect_error!(
solana,
TokenWithdrawInstruction {
amount: 200,
@ -360,12 +350,7 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await;
assert_mango_error(
&res,
MangoError::BankNetBorrowsLimitReached.into(),
"".into(),
MangoError::BankNetBorrowsLimitReached
);
// can borrow smaller amounts: (net borrowed 1000 + new borrow 199) * price 5.0 < limit 6000

View File

@ -177,11 +177,11 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
.await
.unwrap();
last_time = solana.clock_timestamp().await;
assert!(assert_equal_f64_f64(
assert_eq_f64!(
account_position_f64(solana, account, tokens[0].bank).await,
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
0.01
));
);
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
//
@ -208,11 +208,11 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
.await
.unwrap();
//last_time = solana.clock_timestamp().await;
assert!(assert_equal_f64_f64(
assert_eq_f64!(
account_position_f64(solana, account, tokens[0].bank).await,
last_balance * (1.0 - 0.1 * (7.0 / 24.0) * (720.0 / (last_balance * 0.8))),
0.01
));
);
Ok(())
}

View File

@ -185,16 +185,16 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> {
assert_eq!(before_fees_accrued - after_fees_accrued, 19);
// token[1] swapped at discount for token[0]
assert!(assert_equal(
assert_eq_fixed_f64!(
fees_token_position_after - fees_token_position_before,
19.0 / 2.0,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
mngo_token_position_after - mngo_token_position_before,
-19.0 / 3.0 / 1.2,
0.1
));
);
Ok(())
}

View File

@ -357,18 +357,18 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0].quote_position_native(),
-99.99,
0.001
));
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_1.perps[0].quote_position_native(),
99.98,
0.001
));
);
// Market needs to be in force close
assert!(send_tx(
@ -423,18 +423,18 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0].quote_position_native(),
0.009,
0.001
));
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_1.perps[0].quote_position_native(),
-0.0199,
0.001
));
);
Ok(())
}

View File

@ -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(())
}

View File

@ -344,7 +344,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
#[tokio::test]
async fn test_health_compute_serum() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(135_000);
test_builder.test().set_compute_max_units(137_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();

View File

@ -337,11 +337,11 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
liqor_quote_before + 12
);
let acc_data = solana.get_account::<MangoAccount>(account).await;
assert!(assert_equal(
assert_eq_fixed_f64!(
acc_data.perps[0].quote_position_native(),
-50.0 + 11.0 + 27.0,
0.1
));
);
assert_eq!(acc_data.being_liquidated, 0);
let (_liqor_data, liqor_perp) = liqor_info(perp_market, liqor).await;
assert_eq!(liqor_perp.quote_position_native(), -11);

View File

@ -217,35 +217,35 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqee_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
-liqor_amount,
0.1
));
);
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
-20.0 * 100.0 + liqee_amount,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
liqee_data.perps[0].realized_pnl_for_position_native,
liqee_amount - 1000.0,
0.1
));
);
// stable price is 1.0, so 0.2 * 1000
assert_eq!(liqee_data.perps[0].recurring_settle_pnl_allowance, 201);
assert!(assert_equal(
assert_eq_fixed_f64!(
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
liqor_amount - liqee_amount,
0.1,
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
liqor_amount - liqee_amount,
0.1,
));
);
//
// TEST: Liquidate base position max
@ -268,18 +268,18 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqee_amount_2 = 6.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 6);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
-liqor_amount - liqor_amount_2,
0.1
));
);
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
-20.0 * 100.0 + liqee_amount + liqee_amount_2,
0.1
));
);
// verify health is good again
send_tx(
@ -339,28 +339,28 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqee_amount_3 = 10.0 * 100.0 * 1.32 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 16 - 10);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
-liqor_amount - liqor_amount_2 + liqor_amount_3,
0.1
));
);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), -10);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liqee_amount_3,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
liqee_amount_3 - liqor_amount_3,
0.1,
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
liqee_amount_3 - liqor_amount_3,
0.1,
));
);
//
// TEST: Liquidate base position max
@ -383,18 +383,18 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqee_amount_4 = 7.0 * 100.0 * 1.32 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 6 - 7);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4,
0.1
));
);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), -3);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4,
0.1
));
);
// verify health is good again
send_tx(
@ -438,18 +438,18 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqee_amount_5 = 3.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 3);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
-liqor_amount - liqor_amount_2 + liqor_amount_3 + liqor_amount_4 + liqor_amount_5,
0.1
));
);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4 - liqee_amount_5,
0.1
));
);
//
// TEST: Can settle-pnl even though health is negative
@ -481,11 +481,11 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
remaining_pnl,
0.1
));
);
assert_eq!(
account_position(solana, account_1, quote_token.bank).await,
liqee_quote_deposits_before as i64
@ -566,27 +566,27 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
// insurance fund was depleted and the liqor received it
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.tokens[0].native(&quote_bank),
liqor_before.tokens[0].native(&quote_bank).to_num::<f64>() + insurance_vault_funding as f64,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
liqor_data.tokens[1].native(&settle_bank),
liqor_before.tokens[1].native(&settle_bank).to_num::<f64>()
- liqee_settle_limit_before as f64,
0.1
));
);
// liqor took over the max possible negative pnl
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
liqor_before.perps[0]
.quote_position_native()
.to_num::<f64>()
- liq_perp_quote_amount,
0.1
));
);
// liqee exited liquidation
assert!(account_init_health(solana, account_1).await >= 0.0);
@ -602,21 +602,21 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.unwrap();
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
let open_interest = 2 * liqor_data.perps[0].base_position_lots.abs();
assert!(assert_equal(
assert_eq_fixed_f64!(
perp_market.long_funding,
socialized_amount / open_interest as f64,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
perp_market.short_funding,
-socialized_amount / open_interest as f64,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
account0_before.perps[0].unsettled_funding(&perp_market),
socialized_amount / 2.0,
0.1
));
);
Ok(())
}

View File

@ -297,22 +297,22 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 1);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqor_data.perps[0].quote_position_native(),
100.0 + 600.0 - 2100.0 * 0.95,
0.1
));
);
assert_eq!(
account_position(solana, liqor, settle_token.bank).await,
10000 - 95 - 570
);
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 9);
assert!(assert_equal(
assert_eq_fixed_f64!(
liqee_data.perps[0].quote_position_native(),
-10000.0 - 100.0 - 600.0 + 2100.0 * 0.95,
0.1
));
);
assert_eq!(
account_position(solana, account_0, settle_token.bank).await,
95 + 570

View File

@ -421,24 +421,16 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
// The liqee pays for the 20 collateral at a price of 1.02*1.02. The liqor gets 1.01*1.01,
// so the platform fee is
let platform_fee = 20.0 * (1.0 - 1.01 * 1.01 / (1.02 * 1.02));
assert!(assert_equal_f64_f64(
assert_eq_f64!(
account_position_f64(solana, vault_account, collateral_token2.bank).await,
100000.0 + 20.0 - platform_fee,
0.001,
));
);
// Verify platform liq fee tracking
let colbank = solana.get_account::<Bank>(collateral_token2.bank).await;
assert!(assert_equal_fixed_f64(
colbank.collected_fees_native,
platform_fee,
0.001
));
assert!(assert_equal_fixed_f64(
colbank.collected_liquidation_fees,
platform_fee,
0.001
));
assert_eq_fixed_f64!(colbank.collected_fees_native, platform_fee, 0.001);
assert_eq_fixed_f64!(colbank.collected_liquidation_fees, platform_fee, 0.001);
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());

View File

@ -16,9 +16,6 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
let payer_mint0_account = context.users[1].token_accounts[0];
let loan_origination_fee = 0.0005;
// higher resolution that the loan_origination_fee for one token
let balance_f64eq = |a: f64, b: f64| utils::assert_equal_f64_f64(a, b, 0.0001);
//
// SETUP: Create a group, account, register a token (mint0)
//
@ -173,10 +170,11 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
margin_account_initial + withdraw_amount - deposit_amount
);
// no fee because user had positive balance
assert!(balance_f64eq(
assert_eq_f64!(
account_position_f64(solana, account, bank).await,
(deposit_amount_initial - withdraw_amount + deposit_amount) as f64
));
(deposit_amount_initial - withdraw_amount + deposit_amount) as f64,
0.0001
);
//
// TEST: Bringing the balance to 0 deactivates the token
@ -210,10 +208,11 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
solana.token_account_balance(margin_account).await,
margin_account_initial - deposit_amount
);
assert!(balance_f64eq(
assert_eq_f64!(
account_position_f64(solana, account, bank).await,
deposit_amount as f64
));
deposit_amount as f64,
0.0001
);
//
// TEST: Try loan fees by withdrawing more than the user balance
@ -232,11 +231,12 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
solana.token_account_balance(margin_account).await,
margin_account_initial + withdraw_amount - deposit_amount
);
assert!(balance_f64eq(
assert_eq_f64!(
account_position_f64(solana, account, bank).await,
(deposit_amount_initial + deposit_amount - withdraw_amount) as f64
- (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee
));
- (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee,
0.0001
);
Ok(())
}
@ -255,9 +255,6 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
let owner_accounts = context.users[0].token_accounts.clone();
let payer_accounts = context.users[1].token_accounts.clone();
// higher resolution that the loan_origination_fee for one token
let balance_f64eq = |a: f64, b: f64| utils::assert_equal_f64_f64(a, b, 0.0001);
//
// SETUP: Create a group, account, register a token (mint0)
//
@ -414,13 +411,14 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
);
let mango_withdraw_amount = account_position_f64(solana, account, tokens[0].bank).await;
assert!(balance_f64eq(
assert_eq_f64!(
mango_withdraw_amount,
initial_deposit as f64 - withdraw_amount as f64 * (1.0 + swap_fee_rate)
));
initial_deposit as f64 - withdraw_amount as f64 * (1.0 + swap_fee_rate),
0.0001
);
let mango_deposit_amount = account_position_f64(solana, account, tokens[1].bank).await;
assert!(balance_f64eq(mango_deposit_amount, deposit_amount as f64));
assert_eq_f64!(mango_deposit_amount, deposit_amount as f64, 0.0001);
Ok(())
}
@ -732,3 +730,112 @@ async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
Ok(())
}
#[tokio::test]
async fn test_margin_trade_skip_bank() -> Result<(), BanksClientError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(100_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint0_account = context.users[1].token_accounts[0];
//
// SETUP: Create a group, account, register a token (mint0)
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let bank = tokens[0].bank;
//
// create the test user account
//
let deposit_amount_initial = 100;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints,
deposit_amount_initial,
0,
)
.await;
//
// TEST: Margin trade
//
let margin_account = payer_mint0_account;
let target_token_account = context.users[0].token_accounts[0];
let make_flash_loan_tx = |solana, deposit_amount, skip_banks| async move {
let mut tx = ClientTransaction::new(solana);
let loans = vec![FlashLoanPart {
bank,
token_account: target_token_account,
withdraw_amount: 0,
}];
tx.add_instruction(FlashLoanBeginInstruction {
account,
owner,
loans: loans.clone(),
})
.await;
tx.add_instruction_direct(
spl_token::instruction::transfer(
&spl_token::ID,
&margin_account,
&target_token_account,
&payer.pubkey(),
&[&payer.pubkey()],
deposit_amount,
)
.unwrap(),
);
tx.add_signer(payer);
tx.add_instruction(HealthAccountSkipping {
inner: FlashLoanEndInstruction {
account,
owner,
loans,
// the test only accesses a single token: not a swap
flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown,
},
skip_banks,
})
.await;
tx
};
make_flash_loan_tx(solana, 1, vec![])
.await
.send()
.await
.unwrap();
make_flash_loan_tx(solana, 1, vec![tokens[1].bank])
.await
.send()
.await
.unwrap();
make_flash_loan_tx(solana, 1, vec![tokens[0].bank])
.await
.send_expect_error(MangoError::InvalidBank)
.await
.unwrap();
Ok(())
}

View File

@ -287,19 +287,19 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0].quote_position_native(),
-99.99,
0.001
));
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_1.perps[0].quote_position_native(),
99.98,
0.001
));
);
//
// TEST: closing perp positions
@ -364,19 +364,19 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0].quote_position_native(),
0.02,
0.001
));
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_1.perps[0].quote_position_native(),
-0.04,
0.001
));
);
// settle pnl and fees to bring quote_position_native fully to 0
send_tx(
@ -644,19 +644,19 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 2);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0].quote_position_native(),
-19998.0,
0.001
));
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -2);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_1.perps[0].quote_position_native(),
19996.0,
0.001
));
);
//
// TEST: Place a pegged order and check how it behaves with oracle changes
@ -1008,30 +1008,18 @@ async fn test_perp_realize_partially() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.base_position_lots(), 1);
assert!(assert_equal(
assert_eq_fixed_f64!(
perp_0.quote_position_native(),
-200_000.0 + 150_000.0,
0.001
));
assert!(assert_equal(
perp_0.realized_pnl_for_position_native,
50_000.0,
0.001
));
);
assert_eq_fixed_f64!(perp_0.realized_pnl_for_position_native, 50_000.0, 0.001);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_1 = mango_account_1.perps[0];
assert_eq!(perp_1.base_position_lots(), -1);
assert!(assert_equal(
perp_1.quote_position_native(),
200_000.0 - 150_000.0,
0.001
));
assert!(assert_equal(
perp_1.realized_pnl_for_position_native,
-50_000.0,
0.001
));
assert_eq_fixed_f64!(perp_1.quote_position_native(), 200_000.0 - 150_000.0, 0.001);
assert_eq_fixed_f64!(perp_1.realized_pnl_for_position_native, -50_000.0, 0.001);
Ok(())
}
@ -1593,6 +1581,138 @@ async fn test_perp_cancel_with_in_flight_events() -> Result<(), TransportError>
Ok(())
}
#[tokio::test]
async fn test_perp_skip_bank() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// SETUP: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: 0.0000,
taker_fee: 0.0000,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
.unwrap();
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
let price_lots = perp_market_data.native_price_to_lot(I80F48::from(1));
//
// TESTS
//
// good without skips
send_tx(
solana,
HealthAccountSkipping {
inner: PerpPlaceOrderInstruction {
account,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
skip_banks: vec![],
},
)
.await
.unwrap();
// can skip unrelated
send_tx(
solana,
HealthAccountSkipping {
inner: PerpPlaceOrderInstruction {
account,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
skip_banks: vec![tokens[1].bank],
},
)
.await
.unwrap();
// can't skip settle token index
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: PerpPlaceOrderInstruction {
account,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
skip_banks: vec![tokens[0].bank],
},
MangoError::TokenPositionDoesNotExist,
);
Ok(())
}
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;

View File

@ -176,7 +176,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
}
// Cannot settle with yourself
let result = send_tx(
send_tx_expect_error!(
solana,
PerpSettlePnlInstruction {
settler,
@ -185,17 +185,11 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
account_b: account_0,
perp_market,
},
)
.await;
assert_mango_error(
&result,
MangoError::CannotSettleWithSelf.into(),
"Cannot settle with yourself".to_string(),
MangoError::CannotSettleWithSelf
);
// Cannot settle position that does not exist
let result = send_tx(
send_tx_expect_error!(
solana,
PerpSettlePnlInstruction {
settler,
@ -204,13 +198,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
account_b: account_1,
perp_market: perp_market_2,
},
)
.await;
assert_mango_error(
&result,
MangoError::PerpPositionDoesNotExist.into(),
"Cannot settle a position that does not exist".to_string(),
MangoError::PerpPositionDoesNotExist
);
// TODO: Test funding settlement
@ -235,7 +223,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1200.0).await;
// Account a must be the profitable one
let result = send_tx(
send_tx_expect_error!(
solana,
PerpSettlePnlInstruction {
settler,
@ -244,13 +232,7 @@ async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
account_b: account_0,
perp_market,
},
)
.await;
assert_mango_error(
&result,
MangoError::ProfitabilityMismatch.into(),
"Account a must be the profitable one".to_string(),
MangoError::ProfitabilityMismatch
);
// Change the oracle to a more reasonable price
@ -1038,7 +1020,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
// Test 2: Once the settle limit is exhausted, we can't settle more
//
// we are in the same window, and we settled max. possible in previous attempt
let result = send_tx(
send_tx_expect_error!(
solana,
PerpSettlePnlInstruction {
settler,
@ -1047,12 +1029,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_b: account_1,
perp_market,
},
)
.await;
assert_mango_error(
&result,
MangoError::ProfitabilityMismatch.into(),
"Account A has no settleable positive pnl left".to_string(),
MangoError::ProfitabilityMismatch
);
//

View File

@ -166,52 +166,40 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0].quote_position_native(),
-100_020.0,
0.01
));
);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_1.perps[0].quote_position_native(),
100_000.0,
0.01
));
);
// Cannot settle position that does not exist
let result = send_tx(
send_tx_expect_error!(
solana,
PerpSettleFeesInstruction {
account: account_1,
perp_market: perp_market_2,
max_settle_amount: u64::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::PerpPositionDoesNotExist.into(),
"Cannot settle a position that does not exist".to_string(),
MangoError::PerpPositionDoesNotExist
);
// max_settle_amount must be greater than zero
let result = send_tx(
send_tx_expect_error!(
solana,
PerpSettleFeesInstruction {
account: account_1,
perp_market: perp_market,
max_settle_amount: 0,
},
)
.await;
assert_mango_error(
&result,
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
"max_settle_amount must be greater than zero".to_string(),
MangoError::MaxSettleAmountMustBeGreaterThanZero
);
// TODO: Test funding settlement
@ -247,20 +235,20 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
// No change
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert!(assert_equal(
assert_eq_fixed_f64!(
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1200))
.unwrap(),
19980.0, // 1*100*(1200-1000) - (20 in fees)
0.01
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1200))
.unwrap(),
-20000.0,
0.01
));
);
}
// TODO: Difficult to test health due to fees being so small. Need alternative

View File

@ -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(())
}

View File

@ -36,35 +36,39 @@ impl SerumOrderPlacer {
None
}
fn bid_ix(
&mut self,
limit_price: f64,
max_base: u64,
taker: bool,
) -> Serum3PlaceOrderInstruction {
let client_order_id = self.inc_client_order_id();
let fees = if taker { 0.0004 } else { 0.0 };
Serum3PlaceOrderInstruction {
side: Serum3Side::Bid,
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
max_base_qty: max_base / 100, // in base lot (100)
// 4 bps taker fees added in
max_native_quote_qty_including_fees: (limit_price * (max_base as f64) * (1.0 + fees))
.ceil() as u64,
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
order_type: Serum3OrderType::Limit,
client_order_id,
limit: 10,
account: self.account,
owner: self.owner,
serum_market: self.serum_market,
}
}
async fn try_bid(
&mut self,
limit_price: f64,
max_base: u64,
taker: bool,
) -> Result<mango_v4::accounts::Serum3PlaceOrder, TransportError> {
let client_order_id = self.inc_client_order_id();
let fees = if taker { 0.0004 } else { 0.0 };
send_tx(
&self.solana,
Serum3PlaceOrderInstruction {
side: Serum3Side::Bid,
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
max_base_qty: max_base / 100, // in base lot (100)
// 4 bps taker fees added in
max_native_quote_qty_including_fees: (limit_price
* (max_base as f64)
* (1.0 + fees))
.ceil() as u64,
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
order_type: Serum3OrderType::Limit,
client_order_id,
limit: 10,
account: self.account,
owner: self.owner,
serum_market: self.serum_market,
},
)
.await
let ix = self.bid_ix(limit_price, max_base, taker);
send_tx(&self.solana, ix).await
}
async fn bid_maker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
@ -579,7 +583,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
.get_account::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(quote_fees2 - quote_fees1, 0.0, 0.1));
assert_eq_fixed_f64!(quote_fees2 - quote_fees1, 0.0, 0.1);
// check account2 balances too
context
@ -610,11 +614,11 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
.get_account::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(
assert_eq_fixed_f64!(
quote_fees3 - quote_fees1,
loan_origination_fee(fill_amount - deposit_amount) as f64,
0.1
));
);
order_placer.settle().await;
@ -623,11 +627,11 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
.get_account::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(
assert_eq_fixed_f64!(
quote_fees4 - quote_fees3,
serum_fee(fill_amount) as f64,
0.1
));
);
let account_data = solana.get_account::<MangoAccount>(account).await;
assert_eq!(
@ -720,11 +724,11 @@ async fn test_serum_settle_v1() -> Result<(), TransportError> {
.get_account::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(
assert_eq_fixed_f64!(
quote_fees_end - quote_fees_start,
(lof + serum_referrer_fee(amount)) as f64,
0.1
));
);
Ok(())
}
@ -817,11 +821,11 @@ async fn test_serum_settle_v2_to_dao() -> Result<(), TransportError> {
.get_account::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(
assert_eq_fixed_f64!(
quote_fees_end - quote_fees_start,
(lof + serum_referrer_fee(amount)) as f64,
0.1
));
);
let account_data = solana.get_account::<MangoAccount>(account).await;
assert_eq!(
@ -913,11 +917,7 @@ async fn test_serum_settle_v2_to_account() -> Result<(), TransportError> {
.get_account::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(
quote_fees_end - quote_fees_start,
lof as f64,
0.1
));
assert_eq_fixed_f64!(quote_fees_end - quote_fees_start, lof as f64, 0.1);
let account_data = solana.get_account::<MangoAccount>(account).await;
assert_eq!(account_data.buyback_fees_accrued_current, 0);
@ -1029,7 +1029,7 @@ async fn test_serum_reduce_only_deposits1() -> Result<(), TransportError> {
#[tokio::test]
async fn test_serum_reduce_only_deposits2() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
test_builder.test().set_compute_max_units(97_000); // Serum3PlaceOrder needs 95.8k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
@ -1952,6 +1952,71 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_serum_skip_bank() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
//
// SETUP: Create a group, accounts, market etc
//
let deposit_amount = 5000;
let CommonSetup {
group_with_tokens,
mut order_placer,
..
} = common_setup(&context, deposit_amount).await;
let tokens = group_with_tokens.tokens;
//
// TESTS
//
// verify generally good
send_tx(
solana,
HealthAccountSkipping {
inner: order_placer.bid_ix(1.0, 100, false),
skip_banks: vec![],
},
)
.await
.unwrap();
// can skip uninvolved token
send_tx(
solana,
HealthAccountSkipping {
inner: order_placer.bid_ix(1.0, 100, false),
skip_banks: vec![tokens[2].bank],
},
)
.await
.unwrap();
// can't skip base or quote token
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: order_placer.bid_ix(1.0, 100, false),
skip_banks: vec![tokens[0].bank],
},
MangoError::TokenPositionDoesNotExist
);
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: order_placer.bid_ix(1.0, 100, false),
skip_banks: vec![tokens[1].bank],
},
MangoError::TokenPositionDoesNotExist
);
Ok(())
}
struct CommonSetup {
group_with_tokens: GroupWithTokens,
serum_market_cookie: SpotMarketCookie,

View File

@ -321,7 +321,7 @@ async fn test_fallback_oracle_withdraw() -> Result<(), TransportError> {
}
#[tokio::test]
async fn test_clmm_fallback_oracle() -> Result<(), TransportError> {
async fn test_orca_fallback_oracle() -> Result<(), TransportError> {
// add ability to find fixtures
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
@ -340,6 +340,237 @@ async fn test_clmm_fallback_oracle() -> Result<(), TransportError> {
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
"FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH",
),
(
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
),
];
let fallback_oracle = Pubkey::from_str(fixtures[0].0).unwrap();
let pyth_usd_oracle = Pubkey::from_str(fixtures[1].0).unwrap();
let wrong_fallback_oracle = Pubkey::from_str(fixtures[2].0).unwrap();
// setup pyth and clmm accounts
for fixture in fixtures {
let filename = format!("resources/test/{}.bin", fixture.0);
let data = read_file(find_file(&filename).unwrap());
let mut account =
AccountSharedData::new(u64::MAX, data.len(), &Pubkey::from_str(fixture.1).unwrap());
account.set_data(data);
let mut program_test_context = solana.context.borrow_mut();
program_test_context.set_account(&Pubkey::from_str(fixture.0).unwrap(), &account);
}
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..4];
let payer_token_accounts = &context.users[1].token_accounts[0..3];
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;
// add a fallback oracle
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[2].pubkey,
fallback_oracle,
options: mango_v4::instruction::TokenEdit {
set_fallback_oracle: true,
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
let bank_data: Bank = solana.get_account(tokens[2].bank).await;
assert!(bank_data.fallback_oracle == fallback_oracle);
// fill vaults, so we can borrow
let _vault_account = create_funded_account(
&solana,
group,
owner,
2,
&context.users[1],
mints,
100_000,
0,
)
.await;
// Create account with token3 of deposits
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&[mints[2]],
10_000,
0,
)
.await;
// Adjust oracle prices to match CLMM
for i in 0..3 {
send_tx(
solana,
StubOracleSetTestInstruction {
oracle: tokens[i].oracle,
group,
mint: mints[i].pubkey,
admin,
price: 0.06300727055072872,
last_update_slot: 0,
deviation: 0.0,
},
)
.await
.unwrap();
}
// Create some token1 borrows
send_tx(
solana,
TokenWithdrawInstruction {
amount: 100,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
// Make oracle invalid by increasing deviation
send_tx(
solana,
StubOracleSetTestInstruction {
oracle: tokens[2].oracle,
group,
mint: mints[2].pubkey,
admin,
price: 0.06300727055072872,
last_update_slot: 0,
deviation: 100.0,
},
)
.await
.unwrap();
let token_withdraw_ix = TokenWithdrawInstruction {
amount: 1,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[2],
bank_index: 0,
};
// Verify that withdrawing collateral won't work
assert!(send_tx(solana, token_withdraw_ix.clone()).await.is_err());
// Send txn with a fallback oracle in the remaining accounts, but no pyth USD feed
let fallback_oracle_meta = AccountMeta {
pubkey: fallback_oracle,
is_writable: false,
is_signer: false,
};
assert!(send_tx_with_extra_accounts(
solana,
token_withdraw_ix.clone(),
vec![fallback_oracle_meta.clone()]
)
.await
.unwrap()
.result
.is_err());
// add wrong_fallback_oracle for a different token
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[3].pubkey,
fallback_oracle: wrong_fallback_oracle,
options: mango_v4::instruction::TokenEdit {
set_fallback_oracle: true,
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
// Send txn with a the wrong fallback oracle
let wrong_fallback_meta = AccountMeta {
pubkey: wrong_fallback_oracle,
is_writable: false,
is_signer: false,
};
assert!(send_tx_with_extra_accounts(
solana,
token_withdraw_ix.clone(),
vec![wrong_fallback_meta.clone()]
)
.await
.unwrap()
.result
.is_err());
// Finally send txn with a fallback oracle and pyth USD feed
let pyth_usd_oracle_meta = AccountMeta {
pubkey: pyth_usd_oracle,
is_writable: false,
is_signer: false,
};
send_tx_with_extra_accounts(
solana,
token_withdraw_ix,
vec![fallback_oracle_meta, pyth_usd_oracle_meta],
)
.await
.unwrap()
.result
.unwrap();
Ok(())
}
#[tokio::test]
async fn test_raydium_fallback_oracle() -> Result<(), TransportError> {
// add ability to find fixtures
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(150_000); // bad oracles log a lot
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let fixtures = vec![
(
"Ds33rQ1d4AXwxqyeXX6Pc3G4pFNr6iWb3dd8YfBBQMPr",
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK",
),
(
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
"FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH",
),
];
let fallback_oracle = Pubkey::from_str(fixtures[0].0).unwrap();

View File

@ -2,8 +2,6 @@ use super::*;
#[tokio::test]
async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
let context = TestContext::new().await;
let solana = &context.solana.clone();
@ -263,17 +261,17 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
let liqee_quote = account_position_f64(solana, account, quote_token.bank).await;
let liqee_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f_f(
assert_eq_f64!(
liqee_quote,
deposit_amount + 42.0, // roughly 50 / (1.1 * 1.1)
0.01
));
assert!(assert_equal_f_f(liqee_base, deposit_amount - 50.0, 0.01));
);
assert_eq_f64!(liqee_base, deposit_amount - 50.0, 0.01);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f_f(liqor_quote, deposit_amount - 42.0, 0.01));
assert!(assert_equal_f_f(liqor_base, deposit_amount + 44.0, 0.01)); // roughly 42*1.1*0.95
assert_eq_f64!(liqor_quote, deposit_amount - 42.0, 0.01);
assert_eq_f64!(liqor_base, deposit_amount + 44.0, 0.01); // roughly 42*1.1*0.95
//
// TEST: requiring a too-high min buy token execution makes it fail
@ -315,13 +313,13 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
let liqee_quote = account_position_f64(solana, account, quote_token.bank).await;
let liqee_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f_f(liqee_quote, deposit_amount + 84.0, 0.01));
assert!(assert_equal_f_f(liqee_base, deposit_amount - 100.0, 0.01));
assert_eq_f64!(liqee_quote, deposit_amount + 84.0, 0.01);
assert_eq_f64!(liqee_base, deposit_amount - 100.0, 0.01);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f_f(liqor_quote, deposit_amount - 84.0, 0.01));
assert!(assert_equal_f_f(liqor_base, deposit_amount + 88.0, 0.01));
assert_eq_f64!(liqor_quote, deposit_amount - 84.0, 0.01);
assert_eq_f64!(liqor_base, deposit_amount + 88.0, 0.01);
let account_data = get_mango_account(solana, account).await;
assert!(!account_data
@ -334,8 +332,6 @@ async fn test_token_conditional_swap_basic() -> Result<(), TransportError> {
#[tokio::test]
async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportError> {
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
let context = TestContext::new().await;
let solana = &context.solana.clone();
@ -460,7 +456,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
// TEST: Can't take an auction at any price when it's not started yet
//
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
@ -472,12 +468,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
min_buy_token: 0,
min_taker_price: 0.0,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapNotStarted.into(),
"tcs should not be started yet".to_string(),
MangoError::TokenConditionalSwapNotStarted
);
//
@ -507,21 +498,13 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
let account_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f64_f64(
account_quote,
account_quote_expected,
0.1
));
assert!(assert_equal_f64_f64(
account_base,
account_base_expected,
0.1
));
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
assert_eq_f64!(account_base, account_base_expected, 0.1);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
//
// TEST: Stays at end price after end and before expiry
@ -550,27 +533,19 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
let account_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f64_f64(
account_quote,
account_quote_expected,
0.1
));
assert!(assert_equal_f64_f64(
account_base,
account_base_expected,
0.1
));
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
assert_eq_f64!(account_base, account_base_expected, 0.1);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
//
// TEST: Can't take when expired
//
solana.set_clock_timestamp(initial_time + 22).await;
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
@ -582,12 +557,7 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
min_buy_token: 1,
min_taker_price: 0.0,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapExpired.into(),
"tcs should be expired".to_string(),
MangoError::TokenConditionalSwapExpired
);
Ok(())
@ -595,8 +565,6 @@ async fn test_token_conditional_swap_linear_auction() -> Result<(), TransportErr
#[tokio::test]
async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportError> {
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
let context = TestContext::new().await;
let solana = &context.solana.clone();
@ -720,7 +688,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
//
set_bank_stub_oracle_price(solana, group, &base_token, admin, 10.0).await;
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
@ -732,15 +700,10 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
min_buy_token: 0,
min_taker_price: 0.0,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapNotStarted.into(),
"not started yet".to_string(),
MangoError::TokenConditionalSwapNotStarted
);
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapStartInstruction {
liqee: account,
@ -748,19 +711,14 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
liqor_owner: owner,
index: 0,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapPriceNotInRange.into(),
"price not in range".to_string(),
MangoError::TokenConditionalSwapPriceNotInRange
);
//
// TEST: Cannot trigger without start
//
set_bank_stub_oracle_price(solana, group, &base_token, admin, 1.0).await;
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
@ -772,12 +730,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
min_buy_token: 1,
min_taker_price: 0.0,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapNotStarted.into(),
"not started yet".to_string(),
MangoError::TokenConditionalSwapNotStarted
);
send_tx(
@ -815,21 +768,13 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
let account_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f64_f64(
account_quote,
account_quote_expected,
0.1
));
assert!(assert_equal_f64_f64(
account_base,
account_base_expected,
0.1
));
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
assert_eq_f64!(account_base, account_base_expected, 0.1);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
let account_data = get_mango_account(solana, account).await;
let tcs = account_data
@ -866,21 +811,13 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
let account_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f64_f64(
account_quote,
account_quote_expected,
0.1
));
assert!(assert_equal_f64_f64(
account_base,
account_base_expected,
0.1
));
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
assert_eq_f64!(account_base, account_base_expected, 0.1);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
//
// TEST: Premium stops at max increases
@ -910,21 +847,13 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
let account_quote = account_position_f64(solana, account, quote_token.bank).await;
let account_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f64_f64(
account_quote,
account_quote_expected,
0.1
));
assert!(assert_equal_f64_f64(
account_base,
account_base_expected,
0.1
));
assert_eq_f64!(account_quote, account_quote_expected, 0.1);
assert_eq_f64!(account_base, account_base_expected, 0.1);
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f64_f64(liqor_quote, liqor_quote_expected, 0.1));
assert!(assert_equal_f64_f64(liqor_base, liqor_base_expected, 0.1));
assert_eq_f64!(liqor_quote, liqor_quote_expected, 0.1);
assert_eq_f64!(liqor_base, liqor_base_expected, 0.1);
//
// SETUP: make another premium auction to test starting
@ -954,7 +883,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
// TEST: Can't start if oracle not in range
//
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapStartInstruction {
liqee: account,
@ -962,12 +891,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
liqor_owner: owner,
index: 1,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapPriceNotInRange.into(),
"price is not in range".to_string(),
MangoError::TokenConditionalSwapPriceNotInRange
);
//
@ -998,7 +922,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
// TEST: Can't start a second time
//
let res = send_tx(
send_tx_expect_error!(
solana,
TokenConditionalSwapStartInstruction {
liqee: account,
@ -1006,12 +930,7 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
liqor_owner: owner,
index: 1,
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenConditionalSwapAlreadyStarted.into(),
"already started".to_string(),
MangoError::TokenConditionalSwapAlreadyStarted
);
Ok(())
@ -1019,8 +938,6 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
#[tokio::test]
async fn test_token_conditional_swap_deposit_limit() -> Result<(), TransportError> {
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
let context = TestContext::new().await;
let solana = &context.solana.clone();

View File

@ -78,22 +78,22 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
let interest_change = 5000.0 * (dynamic_rate + loan_fee_rate) * diff_ts / year;
let fee_change = 5000.0 * loan_fee_rate * diff_ts / year;
assert!(assert_equal(
assert_eq_fixed_f64!(
bank_after.native_borrows() - bank_before.native_borrows(),
interest_change,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
bank_after.native_deposits() - bank_before.native_deposits(),
interest_change,
0.1
));
assert!(assert_equal(
);
assert_eq_fixed_f64!(
bank_after.collected_fees_native - bank_before.collected_fees_native,
fee_change,
0.1
));
assert!(assert_equal(bank_after.avg_utilization, utilization, 0.01));
);
assert_eq_fixed_f64!(bank_after.avg_utilization, utilization, 0.01);
Ok(())
}
@ -140,19 +140,11 @@ async fn test_token_rates_migrate() -> Result<(), TransportError> {
let bank_after = solana.get_account::<Bank>(tokens[0].bank).await;
assert!(assert_equal_fixed_f64(bank_after.rate0, 0.07 / 3.0, 0.0001));
assert!(assert_equal_fixed_f64(bank_after.rate1, 0.9 / 3.0, 0.0001));
assert!(assert_equal_fixed_f64(bank_after.max_rate, 0.5, 0.0001));
assert!(assert_equal_f64_f64(
bank_after.interest_curve_scaling,
3.0,
0.0001
));
assert!(assert_equal_f64_f64(
bank_after.interest_target_utilization as f64,
0.4,
0.0001
));
assert_eq_fixed_f64!(bank_after.rate0, 0.07 / 3.0, 0.0001);
assert_eq_fixed_f64!(bank_after.rate1, 0.9 / 3.0, 0.0001);
assert_eq_fixed_f64!(bank_after.max_rate, 0.5, 0.0001);
assert_eq_f64!(bank_after.interest_curve_scaling, 3.0, 0.0001);
assert_eq_f64!(bank_after.interest_target_utilization as f64, 0.4, 0.0001);
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
#![allow(dead_code)]
use bytemuck::{bytes_of, Contiguous};
use fixed::types::I80F48;
use solana_program::instruction::InstructionError;
use solana_program::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
@ -97,18 +96,22 @@ pub fn assert_mango_error<T>(
}
}
pub fn assert_equal_fixed_f64(value: I80F48, expected: f64, max_error: f64) -> bool {
let ok = (value.to_num::<f64>() - expected).abs() < max_error;
if !ok {
println!("comparison failed: value: {value}, expected: {expected}");
}
ok
#[macro_export]
macro_rules! assert_eq_f64 {
($value:expr, $expected:expr, $max_error:expr $(,)?) => {
let value = $value;
let expected = $expected;
let ok = (value - expected).abs() < $max_error;
if !ok {
println!("comparison failed: value: {value}, expected: {expected}");
}
assert!(ok);
};
}
pub fn assert_equal_f64_f64(value: f64, expected: f64, max_error: f64) -> bool {
let ok = (value - expected).abs() < max_error;
if !ok {
println!("comparison failed: value: {value}, expected: {expected}");
}
ok
#[macro_export]
macro_rules! assert_eq_fixed_f64 {
($value:expr, $expected:expr, $max_error:expr $(,)?) => {
assert_eq_f64!($value.to_num::<f64>(), $expected, $max_error);
};
}

View File

@ -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
```

View File

@ -51,7 +51,7 @@ async function main() {
market.serumMarketExternal,
);
console.log(
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig.signature}`,
);
}
@ -59,7 +59,7 @@ async function main() {
for (const market of group.perpMarketsMapByMarketIndex.values()) {
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
console.log(
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig.signature}`,
);
}
@ -67,7 +67,7 @@ async function main() {
for (const banks of group.banksMapByMint.values()) {
sig = await client.tokenDeregister(group, banks[0].mint);
console.log(
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`,
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig.signature}`,
);
}
@ -76,13 +76,15 @@ async function main() {
for (const stubOracle of stubOracles) {
sig = await client.stubOracleClose(group, stubOracle.publicKey);
console.log(
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig.signature}`,
);
}
// finally, close the group
sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
console.log(
`Closed group, sig https://explorer.solana.com/tx/${sig.signature}`,
);
}
process.exit();

View File

@ -330,12 +330,12 @@ async function createAndPopulateAlt(
});
let sig = await client.sendAndConfirmTransaction([createIx[0]]);
console.log(
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`,
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig.signature}`,
);
console.log(`ALT: set at index 0 for group...`);
sig = await client.altSet(group, createIx[1], 0);
console.log(`...https://explorer.solana.com/tx/${sig}`);
console.log(`...https://explorer.solana.com/tx/${sig.signature}`);
group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
} catch (error) {
@ -366,7 +366,7 @@ async function createAndPopulateAlt(
addresses,
});
const sig = await client.sendAndConfirmTransaction([extendIx]);
console.log(`https://explorer.solana.com/tx/${sig}`);
console.log(`https://explorer.solana.com/tx/${sig.signature}`);
}
// Extend using mango v4 relevant pub keys

View File

@ -1,18 +1,12 @@
import { PublicKey } from '@solana/web3.js';
import {
HealthType,
MangoAccount,
TokenPosition,
TokenPositionDto,
} from './mangoAccount';
import BN from 'bn.js';
import { Bank, TokenIndex } from './bank';
import { deepClone, toNative, toUiDecimals } from '../utils';
import { expect } from 'chai';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { Group } from './group';
import { HealthCache } from './healthCache';
import { assert } from 'console';
import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { deepClone, toNative, toUiDecimals } from '../utils';
import { Bank, TokenIndex } from './bank';
import { Group } from './group';
import { MangoAccount, TokenPosition } from './mangoAccount';
describe('Mango Account', () => {
const mangoAccount = new MangoAccount(
@ -32,6 +26,7 @@ describe('Mango Account', () => {
new BN(0),
new BN(0),
0,
0,
new BN(0),
[],
[],
@ -106,6 +101,7 @@ describe('maxWithdraw', () => {
new BN(0),
new BN(0),
0,
0,
new BN(0),
[],
[],

View File

@ -51,6 +51,7 @@ export class MangoAccount {
buybackFeesAccruedCurrent: BN;
buybackFeesAccruedPrevious: BN;
buybackFeesExpiryTimestamp: BN;
sequenceNumber: number;
headerVersion: number;
tokens: unknown;
serum3: unknown;
@ -76,6 +77,7 @@ export class MangoAccount {
obj.buybackFeesAccruedCurrent,
obj.buybackFeesAccruedPrevious,
obj.buybackFeesExpiryTimestamp,
obj.sequenceNumber,
obj.headerVersion,
obj.lastCollateralFeeCharge,
obj.tokens as TokenPositionDto[],
@ -103,6 +105,7 @@ export class MangoAccount {
public buybackFeesAccruedCurrent: BN,
public buybackFeesAccruedPrevious: BN,
public buybackFeesExpiryTimestamp: BN,
public sequenceNumber: number,
public headerVersion: number,
public lastCollateralFeeCharge: BN,
tokens: TokenPositionDto[],

View File

@ -83,7 +83,7 @@ import {
import { Id } from './ids';
import { IDL, MangoV4 } from './mango_v4';
import { I80F48 } from './numbers/I80F48';
import { FlashLoanType, OracleConfigParams } from './types';
import { FlashLoanType, HealthCheckKind, OracleConfigParams } from './types';
import {
I64_MAX_BN,
U64_MAX_BN,
@ -1056,6 +1056,50 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async sequenceCheckIx(
group: Group,
mangoAccount: MangoAccount,
): Promise<TransactionInstruction> {
return await this.program.methods
.sequenceCheck(mangoAccount.sequenceNumber)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.instruction();
}
public async healthCheckIx(
group: Group,
mangoAccount: MangoAccount,
minHealthValue: number,
checkKind: HealthCheckKind,
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
await this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[],
[],
[],
);
return await this.program.methods
.healthCheck(minHealthValue, checkKind)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
),
)
.instruction();
}
public async getMangoAccount(
mangoAccountPk: PublicKey,
loadSerum3Oo = false,

View File

@ -310,6 +310,8 @@ export interface IxGateParams {
TokenConditionalSwapCreateLinearAuction: boolean;
Serum3PlaceOrderV2: boolean;
TokenForceWithdraw: boolean;
SequenceCheck: boolean;
HealthCheck: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -390,6 +392,8 @@ export const TrueIxGateParams: IxGateParams = {
TokenConditionalSwapCreateLinearAuction: true,
Serum3PlaceOrderV2: true,
TokenForceWithdraw: true,
SequenceCheck: true,
HealthCheck: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -480,6 +484,8 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
toggleIx(ixGate, p, 'SequenceCheck', 73);
toggleIx(ixGate, p, 'HealthCheck', 74);
return ixGate;
}

View File

@ -1,5 +1,5 @@
export type MangoV4 = {
"version": "0.23.0",
"version": "0.24.0",
"name": "mango_v4",
"instructions": [
{
@ -1760,6 +1760,66 @@ export type MangoV4 = {
}
]
},
{
"name": "sequenceCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group",
"owner"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
}
],
"args": [
{
"name": "expectedSequenceNumber",
"type": "u8"
}
]
},
{
"name": "healthCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": [
{
"name": "minHealthValue",
"type": "f64"
},
{
"name": "checkKind",
"type": {
"defined": "HealthCheckKind"
}
}
]
},
{
"name": "stubOracleCreate",
"accounts": [
@ -7871,13 +7931,8 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
"name": "sequenceNumber",
"type": "u8"
},
{
"name": "netDeposits",
@ -9669,13 +9724,8 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
"name": "sequenceNumber",
"type": "u8"
},
{
"name": "netDeposits",
@ -10654,6 +10704,32 @@ export type MangoV4 = {
]
}
},
{
"name": "HealthCheckKind",
"type": {
"kind": "enum",
"variants": [
{
"name": "Maint"
},
{
"name": "Init"
},
{
"name": "LiquidationEnd"
},
{
"name": "MaintRatio"
},
{
"name": "InitRatio"
},
{
"name": "LiquidationEndRatio"
}
]
}
},
{
"name": "Serum3SelfTradeBehavior",
"docs": [
@ -11008,6 +11084,12 @@ export type MangoV4 = {
},
{
"name": "TokenForceWithdraw"
},
{
"name": "SequenceCheck"
},
{
"name": "HealthCheck"
}
]
}
@ -11048,6 +11130,9 @@ export type MangoV4 = {
},
{
"name": "OrcaCLMM"
},
{
"name": "RaydiumCLMM"
}
]
}
@ -14347,12 +14432,27 @@ export type MangoV4 = {
"code": 6069,
"name": "TokenAssetLiquidationDisabled",
"msg": "the asset does not allow liquidation"
},
{
"code": 6070,
"name": "BorrowsRequireHealthAccountBank",
"msg": "for borrows the bank must be in the health account list"
},
{
"code": 6071,
"name": "InvalidSequenceNumber",
"msg": "invalid sequence number"
},
{
"code": 6072,
"name": "InvalidHealth",
"msg": "invalid health"
}
]
};
export const IDL: MangoV4 = {
"version": "0.23.0",
"version": "0.24.0",
"name": "mango_v4",
"instructions": [
{
@ -16113,6 +16213,66 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "sequenceCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group",
"owner"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
}
],
"args": [
{
"name": "expectedSequenceNumber",
"type": "u8"
}
]
},
{
"name": "healthCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": [
{
"name": "minHealthValue",
"type": "f64"
},
{
"name": "checkKind",
"type": {
"defined": "HealthCheckKind"
}
}
]
},
{
"name": "stubOracleCreate",
"accounts": [
@ -22224,13 +22384,8 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
"name": "sequenceNumber",
"type": "u8"
},
{
"name": "netDeposits",
@ -24022,13 +24177,8 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
1
]
}
"name": "sequenceNumber",
"type": "u8"
},
{
"name": "netDeposits",
@ -25007,6 +25157,32 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "HealthCheckKind",
"type": {
"kind": "enum",
"variants": [
{
"name": "Maint"
},
{
"name": "Init"
},
{
"name": "LiquidationEnd"
},
{
"name": "MaintRatio"
},
{
"name": "InitRatio"
},
{
"name": "LiquidationEndRatio"
}
]
}
},
{
"name": "Serum3SelfTradeBehavior",
"docs": [
@ -25361,6 +25537,12 @@ export const IDL: MangoV4 = {
},
{
"name": "TokenForceWithdraw"
},
{
"name": "SequenceCheck"
},
{
"name": "HealthCheck"
}
]
}
@ -25401,6 +25583,9 @@ export const IDL: MangoV4 = {
},
{
"name": "OrcaCLMM"
},
{
"name": "RaydiumCLMM"
}
]
}
@ -28700,6 +28885,21 @@ export const IDL: MangoV4 = {
"code": 6069,
"name": "TokenAssetLiquidationDisabled",
"msg": "the asset does not allow liquidation"
},
{
"code": 6070,
"name": "BorrowsRequireHealthAccountBank",
"msg": "for borrows the bank must be in the health account list"
},
{
"code": 6071,
"name": "InvalidSequenceNumber",
"msg": "invalid sequence number"
},
{
"code": 6072,
"name": "InvalidHealth",
"msg": "invalid health"
}
]
};

View File

@ -18,6 +18,23 @@ export namespace FlashLoanType {
export const swapWithoutFee = { swapWithoutFee: {} };
}
export type HealthCheckKind =
| { maint: Record<string, never> }
| { init: Record<string, never> }
| { liquidationEnd: Record<string, never> }
| { maintRatio: Record<string, never> }
| { initRatio: Record<string, never> }
| { liquidationEndRatio: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace HealthCheckKind {
export const maint = { maint: {} };
export const init = { init: {} };
export const liquidationEnd = { liquidationEnd: {} };
export const maintRatio = { maintRatio: {} };
export const initRatio = { initRatio: {} };
export const liquidationEndRatio = { liquidationEndRatio: {} };
}
export class InterestRateParams {
util0: number;
rate0: number;