Merge tag 'program-v0.22.0' into deploy

This commit is contained in:
Christian Kamm 2024-03-04 11:32:42 +01:00
commit 27ecc14000
119 changed files with 6612 additions and 1503 deletions

View File

@ -144,17 +144,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
# Report all vulnerabilities in security tab
- name: Report on all vulnerabilities
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: 'Cargo.lock'
ignore-unfixed: true
hide-progress: true
format: 'sarif'
output: 'trivy-results.sarif'
# Fail the job on critical vulnerabiliies with fix available
- name: Fail on critical vulnerabilities
uses: aquasecurity/trivy-action@master
@ -167,12 +156,6 @@ jobs:
severity: 'CRITICAL'
exit-code: '1'
- name: Upload output
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
# Download logs and process them
process-logs:
name: Process logs

View File

@ -59,11 +59,8 @@ jobs:
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Duplicates check
run: yarn deduplicate
run: npx yarn-deduplicate --list --fail
test:
name: Test
@ -87,23 +84,27 @@ jobs:
sast:
name: Security Scan
runs-on: ubuntu-latest
container:
image: returntocorp/semgrep
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript']
steps:
- name: Checkout code
- name: Checkout repository
uses: actions/checkout@v3
- name: Run semgrep
run: semgrep ci --sarif --output=semgrep-results.sarif
env:
SEMGREP_RULES: p/typescript
- name: Upload output
uses: github/codeql-action/upload-sarif@v2
if: always()
- name: Initialise CodeQL
uses: github/codeql-action/init@v2
with:
sarif_file: semgrep-results.sarif
languages: ${{ matrix.language }}
- name: Run CodeQL
uses: github/codeql-action/analyze@v2
sca:
name: Dependency Scan
@ -112,17 +113,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
# Report all vulnerabilities in security tab
- name: Report on all vulnerabilities
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: 'yarn.lock'
ignore-unfixed: true
hide-progress: true
format: 'sarif'
output: 'trivy-results.sarif'
# Fail the job on critical vulnerabiliies with fix available
- name: Fail on critical vulnerabilities
uses: aquasecurity/trivy-action@master
@ -135,15 +125,9 @@ jobs:
severity: 'CRITICAL'
exit-code: '1'
- name: Upload output
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
yarn-pass:
name: Yarn tests pass
needs: ['format', 'lint', 'test']
ts-pass:
name: TS tests pass
needs: ['format', 'lint', 'test', 'depcheck']
runs-on: ubuntu-latest
steps:
- run: echo ok
@ -157,7 +141,7 @@ jobs:
all-pass:
name: All tests pass 🚀
needs: ['yarn-pass', 'security-pass']
needs: ['ts-pass', 'security-pass']
runs-on: ubuntu-latest
steps:
- run: echo ok

View File

@ -12,6 +12,10 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- name: Checkout repo
uses: actions/checkout@v3
- name: Run label config
uses: actions/labeler@v4
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'

View File

@ -4,13 +4,50 @@ Update this for each program release and mainnet deployment.
## not on mainnet
### v0.21.2, 2024-1-
### v0.22.0, 2024-2-
- Allow fast-listing of Openbook v1 markets (#839, #841)
- Perp: Allow reusing your own perp order slots immediately (#817)
Previously users who placed a lot of perp orders and used time-in-force needed
to wait for out-event cranking if their perp order before reusing an order
slot. Now perp order slots can be reused even when the out-event is still on
the event queue.
- Introduce fallback oracles (#790, #813)
Fallback oracles can be used when the primary oracle is stale or not confident.
These oracles need to configured by the DAO to be usable by clients.
Fallback oracles may be based on Orca in addition to the other supported types.
- Add serum3_cancel_by_client_order_id instruction (#798)
Can now cancel by client order id and not just the order id.
- Delegates can now withdraw small token amounts to the owner's ata (#820)
- Custom allocator to allow larger heap use if needed (#801)
- Optimize compute use in token_deposit instruction (#786)
- Disable support for v1 and v2 mango accounts (#783)
- Cleanups, logging and tests (#819, #799, #818, #823, #834, #828, #833)
## mainnet
### v0.21.1, 2024-1-
### v0.21.3, 2024-2-9
Deployment: Feb 9, 2024 at 11:21:58 Central European Standard Time, https://explorer.solana.com/tx/44f2wcLyLiic1aycdaPTdfwXJBMeGeuA984kvCByg4L5iGprH6xW3D35gd3bvZ6kU3SipEtoY3kDuexJghbxL89T
- Remove deposit limit check on Openbook v1 when placing an order to sell
deposits (#869)
### v0.21.2, 2024-1-30
Deployment: Jan 30, 2024 at 12:36:09 Central European Standard Time, https://explorer.solana.com/tx/2kw6XhRUpLbh1fsPyQimCgNWjhy717qnUvxNMtLcBS4VNu8i59AJK4wY7wfZV62gT3GkSRTyaDNyD7Dkrg2gUFxC
- Allow fast-listing of Openbook v1 markets (#839, #841)
### v0.21.1, 2024-1-3
Deployment: Jan 3, 2024 at 14:35:10 Central European Standard Time, https://explorer.solana.com/tx/345NMQAvvtXeuGENz8icErXjGNmgkdU84JpvAMJFWXEGYZ2BNxFFcyZsHp5ELwLNUzY4s2hLa6wxHWPBFsTBLspA
- Prevent withdraw operations from bringing token utilization over 100%.
- Prevent extreme interest rates for tokens with borrows but near zero deposits.

2
Cargo.lock generated
View File

@ -3367,7 +3367,7 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.21.2"
version = "0.22.0"
dependencies = [
"anchor-lang",
"anchor-spl",

View File

@ -11,7 +11,7 @@ anchor-lang = "0.28.0"
anchor-spl = "0.28.0"
fixed = { git = "https://github.com/blockworks-foundation/fixed.git", branch = "v1.11.0-borsh0_10-mango" }
pyth-sdk-solana = "0.8.0"
# commit c85e56d (0.5.10 plus depedency updates)
# commit c85e56d (0.5.10 plus dependency updates)
serum_dex = { git = "https://github.com/openbook-dex/program.git", default-features=false }
mango-feeds-connector = "0.2.1"

View File

@ -704,7 +704,7 @@ describe('mango-v4', () => {
true,
);
// Set price so health is below maintanence
// Set price so health is below maintenance
await envClient.stubOracleSet(group, btcOracle.publicKey, 1);
await mangoAccountB!.reload(clientB);

Binary file not shown.

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use itertools::Itertools;
use mango_v4::accounts_zerocopy::KeyedAccount;
use mango_v4::state::OracleAccountInfos;
use mango_v4_client::{Client, MangoGroupContext};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
@ -30,7 +31,7 @@ pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
.map(|(_, p)| (p.oracle, *p))
.collect();
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
let mut interval = mango_v4_client::delay_interval(std::time::Duration::from_secs(5));
loop {
interval.tick().await;
@ -59,7 +60,9 @@ pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
let perp_opt = perp_markets.get(pubkey);
let mut price = None;
if let Some(bank) = bank_opt {
match bank.oracle_price(&keyed_account, Some(slot)) {
match bank
.oracle_price(&OracleAccountInfos::from_reader(&keyed_account), Some(slot))
{
Ok(p) => price = Some(p),
Err(e) => {
error!("could not read bank oracle {}: {e:?}", keyed_account.key);
@ -67,7 +70,9 @@ pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
}
}
if let Some(perp) = perp_opt {
match perp.oracle_price(&keyed_account, Some(slot)) {
match perp
.oracle_price(&OracleAccountInfos::from_reader(&keyed_account), Some(slot))
{
Ok(p) => price = Some(p),
Err(e) => {
error!("could not read perp oracle {}: {e:?}", keyed_account.key);

View File

@ -12,7 +12,6 @@ use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use tokio::time;
use tracing::*;
use warp::Filter;
@ -155,7 +154,7 @@ pub async fn loop_update_index_and_rate(
token_indices: Vec<TokenIndex>,
interval: u64,
) {
let mut interval = time::interval(Duration::from_secs(interval));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(interval));
loop {
interval.tick().await;
@ -247,7 +246,7 @@ pub async fn loop_consume_events(
perp_market: &PerpMarketContext,
interval: u64,
) {
let mut interval = time::interval(Duration::from_secs(interval));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(interval));
loop {
interval.tick().await;
@ -365,7 +364,7 @@ pub async fn loop_update_funding(
perp_market: &PerpMarketContext,
interval: u64,
) {
let mut interval = time::interval(Duration::from_secs(interval));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(interval));
loop {
interval.tick().await;

View File

@ -116,7 +116,7 @@ async fn main() -> Result<(), anyhow::Error> {
);
let debugging_handle = async {
let mut interval = time::interval(time::Duration::from_secs(5));
let mut interval = mango_v4_client::delay_interval(time::Duration::from_secs(5));
loop {
interval.tick().await;
let client = mango_client.clone();

View File

@ -12,8 +12,6 @@ use mango_v4::{
};
use tracing::*;
use tokio::time;
use crate::MangoClient;
pub async fn runner(
@ -117,7 +115,7 @@ pub async fn loop_blocking_price_update(
token_index: TokenIndex,
price: Arc<RwLock<I80F48>>,
) {
let mut interval = time::interval(Duration::from_secs(1));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(1));
let token_name = &mango_client.context.token(token_index).name;
loop {
interval.tick().await;
@ -135,7 +133,7 @@ pub async fn loop_blocking_orders(
market_name: String,
price: Arc<RwLock<I80F48>>,
) {
let mut interval = time::interval(Duration::from_secs(5));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(5));
// Cancel existing orders
let orders: Vec<u128> = mango_client

View File

@ -70,6 +70,23 @@ impl From<JupiterVersionArg> for jupiter::Version {
}
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum TcsMode {
BorrowBuy,
SwapSellIntoBuy,
SwapCollateralIntoBuy,
}
impl From<TcsMode> for trigger_tcs::Mode {
fn from(a: TcsMode) -> Self {
match a {
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
}
}
}
#[derive(Parser)]
#[clap()]
struct Cli {
@ -124,6 +141,10 @@ struct Cli {
#[clap(long, env, default_value = "0.0005")]
tcs_profit_fraction: f64,
/// control how tcs triggering provides buy tokens
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
tcs_mode: TcsMode,
/// prioritize each transaction with this many microlamports/cu
#[clap(long, env, default_value = "0")]
prioritization_micro_lamports: u64,
@ -327,8 +348,7 @@ async fn main() -> anyhow::Result<()> {
jupiter_version: cli.jupiter_version.into(),
jupiter_slippage_bps: cli.rebalance_slippage_bps,
// TODO: configurable
mode: trigger_tcs::Mode::SwapSellIntoBuy,
mode: cli.tcs_mode.into(),
min_buy_fraction: 0.7,
};
@ -363,29 +383,11 @@ async fn main() -> anyhow::Result<()> {
liquidation_config: liq_config,
trigger_tcs_config: tcs_config,
token_swap_info: token_swap_info_updater.clone(),
liq_errors: ErrorTracking {
skip_threshold: 5,
skip_duration: Duration::from_secs(120),
..ErrorTracking::default()
},
tcs_collection_hard_errors: ErrorTracking {
skip_threshold: 2,
skip_duration: Duration::from_secs(120),
..ErrorTracking::default()
},
tcs_collection_partial_errors: ErrorTracking {
skip_threshold: 2,
skip_duration: Duration::from_secs(120),
..ErrorTracking::default()
},
tcs_execution_errors: ErrorTracking {
skip_threshold: 2,
skip_duration: Duration::from_secs(120),
..ErrorTracking::default()
},
persistent_error_report_interval: Duration::from_secs(300),
persistent_error_min_duration: Duration::from_secs(300),
last_persistent_error_report: Instant::now(),
errors: ErrorTracking::builder()
.skip_threshold(2)
.skip_threshold_for_type(LiqErrorType::Liq, 5)
.skip_duration(Duration::from_secs(120))
.build()?,
});
info!("main loop");
@ -483,7 +485,8 @@ async fn main() -> anyhow::Result<()> {
});
let liquidation_job = tokio::spawn({
let mut interval = tokio::time::interval(Duration::from_millis(cli.check_interval_ms));
let mut interval =
mango_v4_client::delay_interval(Duration::from_millis(cli.check_interval_ms));
let shared_state = shared_state.clone();
async move {
loop {
@ -497,7 +500,7 @@ async fn main() -> anyhow::Result<()> {
state.mango_accounts.iter().cloned().collect_vec()
};
liquidation.log_persistent_errors();
liquidation.errors.update();
let liquidated = liquidation
.maybe_liquidate_one(account_addresses.iter())
@ -526,8 +529,8 @@ async fn main() -> anyhow::Result<()> {
let token_swap_info_job = tokio::spawn({
// TODO: configurable interval
let mut interval = tokio::time::interval(Duration::from_secs(60));
let mut startup_wait = tokio::time::interval(Duration::from_secs(1));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(60));
let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1));
let shared_state = shared_state.clone();
async move {
loop {
@ -544,17 +547,10 @@ async fn main() -> anyhow::Result<()> {
.keys()
.copied()
.collect_vec();
let mut min_delay = tokio::time::interval(Duration::from_secs(1));
let mut min_delay = mango_v4_client::delay_interval(Duration::from_secs(1));
for token_index in token_indexes {
min_delay.tick().await;
match token_swap_info_updater.update_one(token_index).await {
Ok(()) => {}
Err(err) => {
warn!(
"failed to update token swap info for token {token_index}: {err:?}",
);
}
}
token_swap_info_updater.update_one(token_index).await;
}
token_swap_info_updater.log_all();
}
@ -600,6 +596,27 @@ struct SharedState {
one_snapshot_done: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LiqErrorType {
Liq,
/// Errors that suggest we maybe should skip trying to collect tcs for that pubkey
TcsCollectionHard,
/// Recording errors when some tcs have errors during collection but others don't
TcsCollectionPartial,
TcsExecution,
}
impl std::fmt::Display for LiqErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Liq => write!(f, "liq"),
Self::TcsCollectionHard => write!(f, "tcs-collection-hard"),
Self::TcsCollectionPartial => write!(f, "tcs-collection-partial"),
Self::TcsExecution => write!(f, "tcs-execution"),
}
}
}
struct LiquidationState {
mango_client: Arc<MangoClient>,
account_fetcher: Arc<chain_data::AccountFetcher>,
@ -607,15 +624,7 @@ struct LiquidationState {
liquidation_config: liquidate::Config,
trigger_tcs_config: trigger_tcs::Config,
liq_errors: ErrorTracking,
/// Errors that suggest we maybe should skip trying to collect tcs for that pubkey
tcs_collection_hard_errors: ErrorTracking,
/// Recording errors when some tcs have errors during collection but others don't
tcs_collection_partial_errors: ErrorTracking,
tcs_execution_errors: ErrorTracking,
persistent_error_report_interval: Duration,
last_persistent_error_report: Instant,
persistent_error_min_duration: Duration,
errors: ErrorTracking<Pubkey, LiqErrorType>,
}
impl LiquidationState {
@ -646,10 +655,12 @@ impl LiquidationState {
async fn maybe_liquidate_and_log_error(&mut self, pubkey: &Pubkey) -> anyhow::Result<bool> {
let now = Instant::now();
let error_tracking = &mut self.liq_errors;
let error_tracking = &mut self.errors;
// Skip a pubkey if there've been too many errors recently
if let Some(error_entry) = error_tracking.had_too_many_errors(pubkey, now) {
if let Some(error_entry) =
error_tracking.had_too_many_errors(LiqErrorType::Liq, pubkey, now)
{
trace!(
%pubkey,
error_entry.count,
@ -668,7 +679,7 @@ impl LiquidationState {
if let Err(err) = result.as_ref() {
// Keep track of pubkeys that had errors
error_tracking.record_error(pubkey, now, err.to_string());
error_tracking.record(LiqErrorType::Liq, pubkey, err.to_string());
// Not all errors need to be raised to the user's attention.
let mut is_error = true;
@ -691,7 +702,7 @@ impl LiquidationState {
trace!("liquidating account {}: {:?}", pubkey, err);
}
} else {
error_tracking.clear_errors(pubkey);
error_tracking.clear(LiqErrorType::Liq, pubkey);
}
result
@ -720,9 +731,9 @@ impl LiquidationState {
// Find interesting (pubkey, tcsid, volume)
let mut interesting_tcs = Vec::with_capacity(accounts.len());
for pubkey in accounts.iter() {
if let Some(error_entry) = self
.tcs_collection_hard_errors
.had_too_many_errors(pubkey, now)
if let Some(error_entry) =
self.errors
.had_too_many_errors(LiqErrorType::TcsCollectionHard, pubkey, now)
{
trace!(
%pubkey,
@ -734,19 +745,20 @@ impl LiquidationState {
match tcs_context.find_interesting_tcs_for_account(pubkey) {
Ok(v) => {
self.tcs_collection_hard_errors.clear_errors(pubkey);
self.errors.clear(LiqErrorType::TcsCollectionHard, pubkey);
if v.is_empty() {
self.tcs_collection_partial_errors.clear_errors(pubkey);
self.tcs_execution_errors.clear_errors(pubkey);
self.errors
.clear(LiqErrorType::TcsCollectionPartial, pubkey);
self.errors.clear(LiqErrorType::TcsExecution, pubkey);
} else if v.iter().all(|it| it.is_ok()) {
self.tcs_collection_partial_errors.clear_errors(pubkey);
self.errors
.clear(LiqErrorType::TcsCollectionPartial, pubkey);
} else {
for it in v.iter() {
if let Err(e) = it {
info!("error on tcs find_interesting: {:?}", e);
self.tcs_collection_partial_errors.record_error(
self.errors.record(
LiqErrorType::TcsCollectionPartial,
pubkey,
now,
e.to_string(),
);
}
@ -755,8 +767,8 @@ impl LiquidationState {
interesting_tcs.extend(v.iter().filter_map(|it| it.as_ref().ok()));
}
Err(e) => {
self.tcs_collection_hard_errors
.record_error(pubkey, now, e.to_string());
self.errors
.record(LiqErrorType::TcsCollectionHard, pubkey, e.to_string());
}
}
}
@ -765,9 +777,11 @@ impl LiquidationState {
}
let (txsigs, mut changed_pubkeys) = tcs_context
.execute_tcs(&mut interesting_tcs, &mut self.tcs_execution_errors)
.await
.context("execute_tcs")?;
.execute_tcs(&mut interesting_tcs, &mut self.errors)
.await?;
for pubkey in changed_pubkeys.iter() {
self.errors.clear(LiqErrorType::TcsExecution, pubkey);
}
if txsigs.is_empty() {
return Ok(false);
}
@ -793,30 +807,10 @@ impl LiquidationState {
Ok(true)
}
fn log_persistent_errors(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_persistent_error_report)
< self.persistent_error_report_interval
{
return;
}
self.last_persistent_error_report = now;
let min_duration = self.persistent_error_min_duration;
self.liq_errors
.log_persistent_errors("liquidation", min_duration);
self.tcs_execution_errors
.log_persistent_errors("tcs execution", min_duration);
self.tcs_collection_hard_errors
.log_persistent_errors("tcs collection hard", min_duration);
self.tcs_collection_partial_errors
.log_persistent_errors("tcs collection partial", min_duration);
}
}
fn start_chain_data_metrics(chain: Arc<RwLock<chain_data::ChainData>>, metrics: &metrics::Metrics) {
let mut interval = tokio::time::interval(Duration::from_secs(600));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(600));
let mut metric_slots_count = metrics.register_u64("chain_data_slots_count".into());
let mut metric_accounts_count = metrics.register_u64("chain_data_accounts_count".into());

View File

@ -125,7 +125,7 @@ impl Metrics {
}
pub fn start() -> Metrics {
let mut write_interval = time::interval(time::Duration::from_secs(60));
let mut write_interval = mango_v4_client::delay_interval(time::Duration::from_secs(60));
let registry = Arc::new(RwLock::new(HashMap::<String, Value>::new()));
let registry_c = Arc::clone(&registry);

View File

@ -1,8 +1,8 @@
use itertools::Itertools;
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::{
Bank, BookSide, MangoAccountValue, PerpMarket, PerpPosition, PlaceOrderType, Side, TokenIndex,
QUOTE_TOKEN_INDEX,
Bank, BookSide, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpPosition,
PlaceOrderType, Side, TokenIndex, QUOTE_TOKEN_INDEX,
};
use mango_v4_client::{
chain_data, jupiter, perp_pnl, MangoClient, PerpMarketContext, TokenContext,
@ -446,7 +446,8 @@ impl Rebalancer {
// send an ioc order to reduce the base position
let oracle_account_data = self.account_fetcher.fetch_raw(&perp.oracle)?;
let oracle_account = KeyedAccountSharedData::new(perp.oracle, oracle_account_data);
let oracle_price = perp_market.oracle_price(&oracle_account, None)?;
let oracle_price = perp_market
.oracle_price(&OracleAccountInfos::from_reader(&oracle_account), None)?;
let oracle_price_lots = perp_market.native_price_to_lot(oracle_price);
let (side, order_price, oo_lots) = if effective_lots > 0 {
(

View File

@ -4,7 +4,7 @@ use std::sync::Arc;
use tracing::*;
pub async fn report_regularly(client: Arc<MangoClient>, min_health_ratio: f64) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(600));
let mut interval = mango_v4_client::delay_interval(std::time::Duration::from_secs(600));
loop {
interval.tick().await;
if let Err(e) = report(&client, min_health_ratio).await {

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use itertools::Itertools;
use mango_v4_client::error_tracking::ErrorTracking;
use tracing::*;
use mango_v4::state::TokenIndex;
@ -37,21 +38,31 @@ impl TokenSwapInfo {
}
}
struct TokenSwapInfoState {
swap_infos: HashMap<TokenIndex, TokenSwapInfo>,
errors: ErrorTracking<TokenIndex, &'static str>,
}
/// Track the buy/sell slippage for tokens
///
/// Needed to evaluate whether a token conditional swap premium might be good enough
/// without having to query each time.
pub struct TokenSwapInfoUpdater {
mango_client: Arc<MangoClient>,
swap_infos: RwLock<HashMap<TokenIndex, TokenSwapInfo>>,
state: RwLock<TokenSwapInfoState>,
config: Config,
}
const ERROR_TYPE: &'static str = "tsi";
impl TokenSwapInfoUpdater {
pub fn new(mango_client: Arc<MangoClient>, config: Config) -> Self {
Self {
mango_client,
swap_infos: RwLock::new(HashMap::new()),
state: RwLock::new(TokenSwapInfoState {
swap_infos: HashMap::new(),
errors: ErrorTracking::builder().build().unwrap(),
}),
config,
}
}
@ -61,13 +72,13 @@ impl TokenSwapInfoUpdater {
}
fn update(&self, token_index: TokenIndex, slippage: TokenSwapInfo) {
let mut lock = self.swap_infos.write().unwrap();
lock.insert(token_index, slippage);
let mut lock = self.state.write().unwrap();
lock.swap_infos.insert(token_index, slippage);
}
pub fn swap_info(&self, token_index: TokenIndex) -> Option<TokenSwapInfo> {
let lock = self.swap_infos.read().unwrap();
lock.get(&token_index).cloned()
let lock = self.state.read().unwrap();
lock.swap_infos.get(&token_index).cloned()
}
fn in_per_out_price(route: &jupiter::Quote) -> f64 {
@ -76,7 +87,26 @@ impl TokenSwapInfoUpdater {
in_amount / out_amount
}
pub async fn update_one(&self, token_index: TokenIndex) -> anyhow::Result<()> {
pub async fn update_one(&self, token_index: TokenIndex) {
{
let lock = self.state.read().unwrap();
if lock
.errors
.had_too_many_errors(ERROR_TYPE, &token_index, std::time::Instant::now())
.is_some()
{
return;
}
}
if let Err(err) = self.try_update_one(token_index).await {
let mut lock = self.state.write().unwrap();
lock.errors
.record(ERROR_TYPE, &token_index, err.to_string());
}
}
async fn try_update_one(&self, token_index: TokenIndex) -> anyhow::Result<()> {
// since we're only quoting, the slippage does not matter
let slippage = 100;
@ -155,6 +185,11 @@ impl TokenSwapInfoUpdater {
}
pub fn log_all(&self) {
{
let mut lock = self.state.write().unwrap();
lock.errors.update();
}
let mut tokens = self
.mango_client
.context
@ -163,11 +198,12 @@ impl TokenSwapInfoUpdater {
.into_iter()
.collect_vec();
tokens.sort_by(|a, b| a.0.cmp(&b.0));
let infos = self.swap_infos.read().unwrap();
let lock = self.state.read().unwrap();
let mut msg = String::new();
for (token, token_index) in tokens {
let info = infos
let info = lock
.swap_infos
.get(&token_index)
.map(|info| {
format!(

View File

@ -18,7 +18,7 @@ use solana_sdk::{signature::Signature, signer::Signer};
use tracing::*;
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
use crate::{token_swap_info, util, ErrorTracking};
use crate::{token_swap_info, util, ErrorTracking, LiqErrorType};
/// When computing the max possible swap for a liqee, assume the price is this fraction worse for them.
///
@ -65,7 +65,7 @@ pub struct Config {
pub profit_fraction: f64,
/// Minimum fraction of max_buy to buy for success when triggering,
/// useful in conjuction with jupiter swaps in same tx to avoid over-buying.
/// useful in conjunction with jupiter swaps in same tx to avoid over-buying.
///
/// Can be set to 0 to allow executions of any size.
pub min_buy_fraction: f64,
@ -962,10 +962,9 @@ impl Context {
pub async fn execute_tcs(
&self,
tcs: &mut [(Pubkey, u64, u64)],
error_tracking: &mut ErrorTracking,
error_tracking: &mut ErrorTracking<Pubkey, LiqErrorType>,
) -> anyhow::Result<(Vec<Signature>, Vec<Pubkey>)> {
use rand::distributions::{Distribution, WeightedError, WeightedIndex};
let now = Instant::now();
let max_volume = self.config.max_trigger_quote_amount;
let mut pending_volume = 0;
@ -1012,7 +1011,11 @@ impl Context {
}
Err(e) => {
trace!(%result.pubkey, "preparation error {:?}", e);
error_tracking.record_error(&result.pubkey, now, e.to_string());
error_tracking.record(
LiqErrorType::TcsExecution,
&result.pubkey,
e.to_string(),
);
}
}
no_new_job = false;
@ -1089,7 +1092,7 @@ impl Context {
Ok(v) => Some((pubkey, v)),
Err(err) => {
trace!(%pubkey, "execution error {:?}", err);
error_tracking.record_error(&pubkey, Instant::now(), err.to_string());
error_tracking.record(LiqErrorType::TcsExecution, &pubkey, err.to_string());
None
}
});
@ -1104,10 +1107,12 @@ impl Context {
pubkey: &Pubkey,
tcs_id: u64,
volume: u64,
error_tracking: &ErrorTracking,
error_tracking: &ErrorTracking<Pubkey, LiqErrorType>,
) -> Option<Pin<Box<dyn Future<Output = PreparationResult> + Send>>> {
// Skip a pubkey if there've been too many errors recently
if let Some(error_entry) = error_tracking.had_too_many_errors(pubkey, Instant::now()) {
if let Some(error_entry) =
error_tracking.had_too_many_errors(LiqErrorType::TcsExecution, pubkey, Instant::now())
{
trace!(
"skip checking for tcs on account {pubkey}, had {} errors recently",
error_entry.count

View File

@ -80,7 +80,7 @@ Fill Event
}
```
If the fill ocurred on a fork, an event will be sent with the 'status' field set to 'revoke'.
If the fill occurred on a fork, an event will be sent with the 'status' field set to 'revoke'.
## Setup

View File

@ -571,7 +571,7 @@ async fn main() -> anyhow::Result<()> {
// keepalive
{
tokio::spawn(async move {
let mut write_interval = time::interval(time::Duration::from_secs(30));
let mut write_interval = mango_v4_client::delay_interval(time::Duration::from_secs(30));
loop {
write_interval.tick().await;

View File

@ -544,7 +544,7 @@ async fn main() -> anyhow::Result<()> {
let exit = exit.clone();
let peers = peers.clone();
tokio::spawn(async move {
let mut write_interval = time::interval(time::Duration::from_secs(30));
let mut write_interval = mango_v4_client::delay_interval(time::Duration::from_secs(30));
loop {
if exit.load(Ordering::Relaxed) {

View File

@ -13,8 +13,8 @@ use mango_feeds_lib::{
OrderbookSide,
};
use mango_v4::accounts_zerocopy::{AccountReader, KeyedAccountReader};
use mango_v4::state::oracle_state_unchecked;
use mango_v4::state::OracleConfigParams;
use mango_v4::state::{oracle_state_unchecked, OracleAccountInfos};
use mango_v4::{
serum3_cpi::OrderBookStateHeader,
state::{BookSide, OrderTreeType},
@ -368,12 +368,12 @@ pub async fn init(
max_staleness_slots: None, // don't check oracle staleness to get an orderbook
};
if let Ok(unchecked_oracle_state) =
oracle_state_unchecked(&keyed_account, mkt.1.base_decimals)
{
if let Ok(unchecked_oracle_state) = oracle_state_unchecked(
&OracleAccountInfos::from_reader(&keyed_account),
mkt.1.base_decimals,
) {
if unchecked_oracle_state
.check_confidence_and_maybe_staleness(
&oracle_pk,
&oracle_config.to_oracle_config(),
None, // force this to always return a price no matter how stale
)

View File

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use std::time::Duration;
use anchor_client::Cluster;
use clap::Parser;
@ -216,15 +216,12 @@ async fn main() -> anyhow::Result<()> {
mango_client: mango_client.clone(),
account_fetcher: account_fetcher.clone(),
config: tcs_start::Config {
persistent_error_min_duration: Duration::from_secs(300),
persistent_error_report_interval: Duration::from_secs(300),
},
errors: mango_v4_client::error_tracking::ErrorTracking {
skip_threshold: 2,
skip_duration: Duration::from_secs(60),
..Default::default()
},
last_persistent_error_report: Instant::now(),
errors: mango_v4_client::error_tracking::ErrorTracking::builder()
.skip_threshold(2)
.skip_duration(Duration::from_secs(60))
.build()?,
};
info!("main loop");
@ -296,7 +293,7 @@ async fn main() -> anyhow::Result<()> {
});
let settle_job = tokio::spawn({
let mut interval = tokio::time::interval(Duration::from_millis(100));
let mut interval = mango_v4_client::delay_interval(Duration::from_millis(100));
let shared_state = shared_state.clone();
async move {
loop {
@ -319,7 +316,7 @@ async fn main() -> anyhow::Result<()> {
});
let tcs_start_job = tokio::spawn({
let mut interval = tokio::time::interval(Duration::from_millis(100));
let mut interval = mango_v4_client::delay_interval(Duration::from_millis(100));
let shared_state = shared_state.clone();
async move {
loop {
@ -373,7 +370,7 @@ struct SharedState {
}
fn start_chain_data_metrics(chain: Arc<RwLock<chain_data::ChainData>>, metrics: &metrics::Metrics) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(600));
let mut interval = mango_v4_client::delay_interval(std::time::Duration::from_secs(600));
let mut metric_slots_count = metrics.register_u64("chain_data_slots_count".into());
let mut metric_accounts_count = metrics.register_u64("chain_data_accounts_count".into());

View File

@ -125,7 +125,7 @@ impl Metrics {
}
pub fn start() -> Metrics {
let mut write_interval = time::interval(time::Duration::from_secs(60));
let mut write_interval = mango_v4_client::delay_interval(time::Duration::from_secs(60));
let registry = Arc::new(RwLock::new(HashMap::<String, Value>::new()));
let registry_c = Arc::clone(&registry);

View File

@ -4,7 +4,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::HealthType;
use mango_v4::state::{PerpMarket, PerpMarketIndex};
use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex};
use mango_v4_client::{
chain_data, health_cache, prettify_solana_client_error, MangoClient, PreparedInstructions,
TransactionBuilder,
@ -34,11 +34,10 @@ fn perp_markets_and_prices(
.map(|(market_index, perp)| {
let perp_market = account_fetcher.fetch::<PerpMarket>(&perp.address)?;
let oracle_acc = account_fetcher.fetch_raw(&perp_market.oracle)?;
let oracle_price = perp_market.oracle_price(
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
None,
)?;
let oracle = account_fetcher.fetch_raw(&perp_market.oracle)?;
let oracle_acc = &KeyedAccountSharedData::new(perp_market.oracle, oracle);
let oracle_price =
perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
let settle_token = mango_client.context.token(perp_market.settle_token_index);
let settle_token_price =

View File

@ -12,7 +12,19 @@ use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
pub struct Config {
pub persistent_error_report_interval: Duration,
pub persistent_error_min_duration: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorType {
StartTcs,
}
impl std::fmt::Display for ErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::StartTcs => write!(f, "start-tcs"),
}
}
}
pub struct State {
@ -20,8 +32,7 @@ pub struct State {
pub account_fetcher: Arc<chain_data::AccountFetcher>,
pub config: Config,
pub errors: ErrorTracking,
pub last_persistent_error_report: Instant,
pub errors: ErrorTracking<Pubkey, ErrorType>,
}
impl State {
@ -33,23 +44,10 @@ impl State {
}
self.run_pass_inner(&accounts).await?;
self.log_persistent_errors();
self.errors.update();
Ok(())
}
fn log_persistent_errors(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_persistent_error_report)
< self.config.persistent_error_report_interval
{
return;
}
self.last_persistent_error_report = now;
let min_duration = self.config.persistent_error_min_duration;
self.errors.log_persistent_errors("start_tcs", min_duration);
}
async fn run_pass_inner(&mut self, accounts: &[Pubkey]) -> anyhow::Result<()> {
let now_ts: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let now = Instant::now();
@ -69,7 +67,11 @@ impl State {
if account.fixed.group != mango_client.group() {
continue;
}
if self.errors.had_too_many_errors(account_key, now).is_some() {
if self
.errors
.had_too_many_errors(ErrorType::StartTcs, account_key, now)
.is_some()
{
continue;
}
@ -79,9 +81,9 @@ impl State {
Ok(true) => {}
Ok(false) => continue,
Err(e) => {
self.errors.record_error(
self.errors.record(
ErrorType::StartTcs,
account_key,
now,
format!("error in is_tcs_startable: tcsid={}, {e:?}", tcs.id),
);
}
@ -91,7 +93,7 @@ impl State {
}
if !had_tcs {
self.errors.clear_errors(account_key);
self.errors.clear(ErrorType::StartTcs, account_key);
}
}
@ -108,9 +110,9 @@ impl State {
let ixs = match self.make_start_ix(pubkey, *tcs_id).await {
Ok(v) => v,
Err(e) => {
self.errors.record_error(
self.errors.record(
ErrorType::StartTcs,
pubkey,
now,
format!("error in make_start_ix: tcsid={tcs_id}, {e:?}"),
);
continue;
@ -148,9 +150,9 @@ impl State {
.iter()
.filter_map(|(pk, tcs_id)| (pk == pubkey).then_some(tcs_id))
.collect_vec();
self.errors.record_error(
self.errors.record(
ErrorType::StartTcs,
pubkey,
now,
format!("error sending transaction: tcsids={tcs_ids:?}, {e:?}"),
);
}
@ -162,7 +164,7 @@ impl State {
// clear errors on pubkeys with successes
for pubkey in ix_targets.iter().map(|(pk, _)| pk).unique() {
self.errors.clear_errors(pubkey);
self.errors.clear(ErrorType::StartTcs, pubkey);
}
}

View File

@ -8,7 +8,7 @@ use anchor_lang::Discriminator;
use fixed::types::I80F48;
use mango_v4::accounts_zerocopy::{KeyedAccountSharedData, LoadZeroCopy};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, OracleAccountInfos};
use anyhow::Context;
@ -65,7 +65,8 @@ impl AccountFetcher {
pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> {
let bank: Bank = self.fetch(bank)?;
let oracle = self.fetch_raw(&bank.oracle)?;
let price = bank.oracle_price(&KeyedAccountSharedData::new(bank.oracle, oracle), None)?;
let oracle_acc = &KeyedAccountSharedData::new(bank.oracle, oracle.into());
let price = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
Ok((bank, price))
}

View File

@ -18,8 +18,8 @@ use itertools::Itertools;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::{
Bank, Group, MangoAccountValue, PerpMarket, PerpMarketIndex, PlaceOrderType, SelfTradeBehavior,
Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX,
Bank, Group, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpMarketIndex,
PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX,
};
use solana_address_lookup_table_program::state::AddressLookupTable;
@ -499,10 +499,8 @@ impl MangoClient {
.account_fetcher
.fetch_raw_account(&mint_info.oracle)
.await?;
let price = bank.oracle_price(
&KeyedAccountSharedData::new(mint_info.oracle, oracle.into()),
None,
)?;
let oracle_acc = &KeyedAccountSharedData::new(mint_info.oracle, oracle.into());
let price = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
Ok(price)
}
@ -514,10 +512,8 @@ impl MangoClient {
let perp_market: PerpMarket =
account_fetcher_fetch_anchor_account(&*self.account_fetcher, &perp.address).await?;
let oracle = self.account_fetcher.fetch_raw_account(&perp.oracle).await?;
let price = perp_market.oracle_price(
&KeyedAccountSharedData::new(perp.oracle, oracle.into()),
None,
)?;
let oracle_acc = &KeyedAccountSharedData::new(perp.oracle, oracle.into());
let price = perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
Ok(price)
}
@ -1710,7 +1706,7 @@ impl MangoClient {
mango_client: Arc<MangoClient>,
interval: Duration,
) {
let mut delay = tokio::time::interval(interval);
let mut delay = crate::delay_interval(interval);
let rpc_async = mango_client.client.rpc_async();
loop {
delay.tick().await;

View File

@ -227,7 +227,7 @@ impl MangoGroupContext {
decimals: u8::MAX,
banks: mi.banks,
vaults: mi.vaults,
fallback_oracle: Pubkey::default(), // coming in v0.22
fallback_oracle: mi.fallback_oracle,
oracle: mi.oracle,
group: mi.group,
mint: mi.mint,

View File

@ -1,4 +1,3 @@
use anchor_lang::prelude::Pubkey;
use std::{
collections::HashMap,
time::{Duration, Instant},
@ -6,68 +5,177 @@ use std::{
use tracing::*;
#[derive(Clone)]
pub struct AccountErrorState {
pub messages: Vec<String>,
pub struct ErrorState {
pub errors: Vec<String>,
pub count: u64,
pub last_at: Instant,
}
#[derive(Default)]
pub struct ErrorTracking {
pub accounts: HashMap<Pubkey, AccountErrorState>,
#[derive(Clone)]
struct ErrorTypeState<Key> {
state_by_key: HashMap<Key, ErrorState>,
// override global
skip_threshold: Option<u64>,
skip_duration: Option<Duration>,
}
impl<Key> Default for ErrorTypeState<Key> {
fn default() -> Self {
Self {
state_by_key: Default::default(),
skip_threshold: None,
skip_duration: None,
}
}
}
#[derive(Builder)]
pub struct ErrorTracking<Key, ErrorType> {
#[builder(setter(custom))]
errors_by_type: HashMap<ErrorType, ErrorTypeState<Key>>,
/// number of errors of a type after which had_too_many_errors returns true
#[builder(default = "2")]
pub skip_threshold: u64,
/// duration that had_too_many_errors returns true for after skip_threshold is reached
#[builder(default = "Duration::from_secs(60)")]
pub skip_duration: Duration,
#[builder(default = "3")]
pub unique_messages_to_keep: usize,
/// after what time of no-errors may error info be wiped?
#[builder(default = "Duration::from_secs(300)")]
pub keep_duration: Duration,
#[builder(setter(skip), default = "Instant::now()")]
last_log: Instant,
#[builder(default = "Duration::from_secs(300)")]
pub log_interval: Duration,
}
impl ErrorTracking {
pub fn had_too_many_errors(&self, pubkey: &Pubkey, now: Instant) -> Option<AccountErrorState> {
if let Some(error_entry) = self.accounts.get(pubkey) {
if error_entry.count >= self.skip_threshold
&& now.duration_since(error_entry.last_at) < self.skip_duration
{
Some(error_entry.clone())
} else {
None
}
} else {
None
impl<Key, ErrorType> ErrorTrackingBuilder<Key, ErrorType>
where
ErrorType: Copy + std::hash::Hash + std::cmp::Eq + std::fmt::Display,
{
pub fn skip_threshold_for_type(&mut self, error_type: ErrorType, threshold: u64) -> &mut Self {
if self.errors_by_type.is_none() {
self.errors_by_type = Some(Default::default());
}
let errors_by_type = self.errors_by_type.as_mut().unwrap();
errors_by_type.entry(error_type).or_default().skip_threshold = Some(threshold);
self
}
}
impl<Key, ErrorType> ErrorTracking<Key, ErrorType>
where
Key: Clone + std::hash::Hash + std::cmp::Eq + std::fmt::Display,
ErrorType: Copy + std::hash::Hash + std::cmp::Eq + std::fmt::Display,
{
pub fn builder() -> ErrorTrackingBuilder<Key, ErrorType> {
ErrorTrackingBuilder::default()
}
pub fn record_error(&mut self, pubkey: &Pubkey, now: Instant, message: String) {
let error_entry = self.accounts.entry(*pubkey).or_insert(AccountErrorState {
messages: Vec::with_capacity(1),
count: 0,
last_at: now,
});
error_entry.count += 1;
error_entry.last_at = now;
if !error_entry.messages.contains(&message) {
error_entry.messages.push(message);
}
if error_entry.messages.len() > 5 {
error_entry.messages.remove(0);
}
fn should_skip(
&self,
state: &ErrorState,
error_type_state: &ErrorTypeState<Key>,
now: Instant,
) -> bool {
let skip_threshold = error_type_state
.skip_threshold
.unwrap_or(self.skip_threshold);
let skip_duration = error_type_state.skip_duration.unwrap_or(self.skip_duration);
state.count >= skip_threshold && now.duration_since(state.last_at) < skip_duration
}
pub fn clear_errors(&mut self, pubkey: &Pubkey) {
self.accounts.remove(pubkey);
pub fn had_too_many_errors(
&self,
error_type: ErrorType,
key: &Key,
now: Instant,
) -> Option<ErrorState> {
let error_type_state = self.errors_by_type.get(&error_type)?;
let state = error_type_state.state_by_key.get(key)?;
self.should_skip(state, error_type_state, now)
.then(|| state.clone())
}
#[instrument(skip_all, fields(%error_type))]
#[allow(unused_variables)]
pub fn log_persistent_errors(&self, error_type: &str, min_duration: Duration) {
pub fn record(&mut self, error_type: ErrorType, key: &Key, message: String) {
let now = Instant::now();
for (pubkey, errors) in self.accounts.iter() {
if now.duration_since(errors.last_at) < min_duration {
continue;
let state = self
.errors_by_type
.entry(error_type)
.or_default()
.state_by_key
.entry(key.clone())
.or_insert(ErrorState {
errors: Vec::with_capacity(1),
count: 0,
last_at: now,
});
state.count += 1;
state.last_at = now;
if let Some(pos) = state.errors.iter().position(|m| m == &message) {
state.errors.remove(pos);
}
state.errors.push(message);
if state.errors.len() > self.unique_messages_to_keep {
state.errors.remove(0);
}
// log when skip threshold is reached the first time
if state.count == self.skip_threshold {
trace!(%error_type, %key, count = state.count, messages = ?state.errors, "had repeated errors, skipping...");
}
}
pub fn clear(&mut self, error_type: ErrorType, key: &Key) {
if let Some(error_type_state) = self.errors_by_type.get_mut(&error_type) {
error_type_state.state_by_key.remove(key);
}
}
pub fn wipe_old(&mut self) {
let now = Instant::now();
for error_type_state in self.errors_by_type.values_mut() {
error_type_state
.state_by_key
.retain(|_, state| now.duration_since(state.last_at) < self.keep_duration);
}
}
/// Wipes old errors and occasionally logs errors that caused skipping
pub fn update(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_log) < self.log_interval {
return;
}
self.last_log = now;
self.wipe_old();
self.log_error_skips();
}
/// Log all errors that cause skipping
pub fn log_error_skips(&self) {
let now = Instant::now();
for (error_type, error_type_state) in self.errors_by_type.iter() {
let span = info_span!("log_error_skips", %error_type);
let _enter = span.enter();
for (key, state) in error_type_state.state_by_key.iter() {
if self.should_skip(state, error_type_state, now) {
info!(
%key,
count = state.count,
messages = ?state.errors,
);
}
}
info!(
%pubkey,
count = errors.count,
messages = ?errors.messages,
"has persistent errors",
);
}
}
}

View File

@ -34,6 +34,9 @@ pub async fn new(
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None,
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts
usd_oracle_index: None,
sol_oracle_index: None,
};
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts)
@ -67,6 +70,9 @@ pub fn new_sync(
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None,
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts
usd_oracle_index: None,
sol_oracle_index: None,
};
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts)

View File

@ -28,13 +28,12 @@ pub async fn fetch_top(
let perp = context.perp(perp_market_index);
let perp_market =
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address).await?;
let oracle_acc = account_fetcher
let oracle = account_fetcher
.fetch_raw_account(&perp_market.oracle)
.await?;
let oracle_price = perp_market.oracle_price(
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
None,
)?;
let oracle_acc = &KeyedAccountSharedData::new(perp.oracle, oracle.into());
let oracle_price =
perp_market.oracle_price(&&OracleAccountInfos::from_reader(oracle_acc), None)?;
let accounts = account_fetcher
.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())

View File

@ -222,8 +222,8 @@ async fn feed_snapshots(
}
pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::Sender<Message>) {
let mut poll_wait_first_snapshot = time::interval(time::Duration::from_secs(2));
let mut interval_between_snapshots = time::interval(config.snapshot_interval);
let mut poll_wait_first_snapshot = crate::delay_interval(time::Duration::from_secs(2));
let mut interval_between_snapshots = crate::delay_interval(config.snapshot_interval);
tokio::spawn(async move {
let rpc_client = http::connect_with_options::<MinimalClient>(&config.rpc_http_url, true)

View File

@ -43,6 +43,20 @@ impl<T> AsyncChannelSendUnlessFull<T> for async_channel::Sender<T> {
}
}
/// Like tokio::time::interval(), but with Delay as default MissedTickBehavior
///
/// The default (Burst) means that if the time between tick() calls is longer
/// than `period` there'll be a burst of catch-up ticks.
///
/// This Interval guarantees that when tick() returns, at least `period` will have
/// elapsed since the last return. That way it's more appropriate for jobs that
/// don't need to catch up.
pub fn delay_interval(period: std::time::Duration) -> tokio::time::Interval {
let mut interval = tokio::time::interval(period);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval
}
/// A copy of RpcClient::send_and_confirm_transaction that returns the slot the
/// transaction confirmed in.
pub fn send_and_confirm_transaction(

View File

@ -1,5 +1,5 @@
{
"version": "0.21.2",
"version": "0.22.0",
"name": "mango_v4",
"instructions": [
{
@ -485,6 +485,11 @@
"isMut": false,
"isSigner": false
},
{
"name": "fallbackOracle",
"isMut": false,
"isSigner": false
},
{
"name": "payer",
"isMut": true,
@ -618,6 +623,14 @@
{
"name": "depositLimit",
"type": "u64"
},
{
"name": "zeroUtilRate",
"type": "f32"
},
{
"name": "platformLiquidationFee",
"type": "f32"
}
]
},
@ -727,6 +740,11 @@
"isMut": false,
"isSigner": false
},
{
"name": "fallbackOracle",
"isMut": false,
"isSigner": false
},
{
"name": "payer",
"isMut": true,
@ -788,6 +806,15 @@
"The oracle account is optional and only used when reset_stable_price is set.",
""
]
},
{
"name": "fallbackOracle",
"isMut": false,
"isSigner": false,
"docs": [
"The fallback oracle account is optional and only used when set_fallback_oracle is true.",
""
]
}
],
"args": [
@ -1002,6 +1029,18 @@
"type": {
"option": "u64"
}
},
{
"name": "zeroUtilRateOpt",
"type": {
"option": "f32"
}
},
{
"name": "platformLiquidationFeeOpt",
"type": {
"option": "f32"
}
}
]
},
@ -1703,27 +1742,7 @@
{
"name": "oracle",
"isMut": true,
"isSigner": false,
"pda": {
"seeds": [
{
"kind": "const",
"type": "string",
"value": "StubOracle"
},
{
"kind": "account",
"type": "publicKey",
"path": "group"
},
{
"kind": "account",
"type": "publicKey",
"account": "Mint",
"path": "mint"
}
]
}
"isSigner": true
},
{
"name": "admin",
@ -2014,8 +2033,7 @@
"isMut": true,
"isSigner": false,
"relations": [
"group",
"owner"
"group"
]
},
{
@ -2997,6 +3015,75 @@
}
]
},
{
"name": "serum3CancelOrderByClientOrderId",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "openOrders",
"isMut": true,
"isSigner": false
},
{
"name": "serumMarket",
"isMut": false,
"isSigner": false,
"relations": [
"group",
"serum_program",
"serum_market_external"
]
},
{
"name": "serumProgram",
"isMut": false,
"isSigner": false
},
{
"name": "serumMarketExternal",
"isMut": true,
"isSigner": false
},
{
"name": "marketBids",
"isMut": true,
"isSigner": false
},
{
"name": "marketAsks",
"isMut": true,
"isSigner": false
},
{
"name": "marketEventQueue",
"isMut": true,
"isSigner": false
}
],
"args": [
{
"name": "clientOrderId",
"type": "u64"
}
]
},
{
"name": "serum3CancelAllOrders",
"accounts": [
@ -3866,6 +3953,10 @@
{
"name": "positivePnlLiquidationFee",
"type": "f32"
},
{
"name": "platformLiquidationFee",
"type": "f32"
}
]
},
@ -4080,6 +4171,12 @@
"type": {
"option": "bool"
}
},
{
"name": "platformLiquidationFeeOpt",
"type": {
"option": "f32"
}
}
]
},
@ -7070,6 +7167,16 @@
},
{
"name": "util0",
"docs": [
"The unscaled borrow interest curve is defined as continuous piecewise linear with the points:",
"",
"- 0% util: zero_util_rate",
"- util0% util: rate0",
"- util1% util: rate1",
"- 100% util: max_rate",
"",
"The final rate is this unscaled curve multiplied by interest_curve_scaling."
],
"type": {
"defined": "I80F48"
}
@ -7094,12 +7201,24 @@
},
{
"name": "maxRate",
"docs": [
"the 100% utilization rate",
"",
"This isn't the max_rate, since this still gets scaled by interest_curve_scaling,",
"which is >=1."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collectedFeesNative",
"docs": [
"Fees collected over the lifetime of the bank",
"",
"See fees_withdrawn for how much of the fees was withdrawn.",
"See collected_liquidation_fees for the (included) subtotal for liquidation related fees."
],
"type": {
"defined": "I80F48"
}
@ -7142,6 +7261,15 @@
},
{
"name": "liquidationFee",
"docs": [
"Liquidation fee that goes to the liqor.",
"",
"Liquidation always involves two tokens, and the sum of the two configured fees is used.",
"",
"A fraction of the price, like 0.05 for a 5% fee during liquidation.",
"",
"See also platform_liquidation_fee."
],
"type": {
"defined": "I80F48"
}
@ -7300,20 +7428,35 @@
},
{
"name": "maintWeightShiftStart",
"docs": [
"Start timestamp in seconds at which maint weights should start to change away",
"from maint_asset_weight, maint_liab_weight towards _asset_target and _liab_target.",
"If _start and _end and _duration_inv are 0, no shift is configured."
],
"type": "u64"
},
{
"name": "maintWeightShiftEnd",
"docs": [
"End timestamp in seconds until which the maint weights should reach the configured targets."
],
"type": "u64"
},
{
"name": "maintWeightShiftDurationInv",
"docs": [
"Cache of the inverse of maint_weight_shift_end - maint_weight_shift_start,",
"or zero if no shift is configured"
],
"type": {
"defined": "I80F48"
}
},
{
"name": "maintWeightShiftAssetTarget",
"docs": [
"Maint asset weight to reach at _shift_end."
],
"type": {
"defined": "I80F48"
}
@ -7326,6 +7469,10 @@
},
{
"name": "fallbackOracle",
"docs": [
"Oracle that may be used if the main oracle is stale or not confident enough.",
"If this is Pubkey::default(), no fallback is available."
],
"type": "publicKey"
},
{
@ -7335,12 +7482,43 @@
],
"type": "u64"
},
{
"name": "zeroUtilRate",
"docs": [
"The unscaled borrow interest curve point for zero utilization.",
"",
"See util0, rate0, util1, rate1, max_rate"
],
"type": {
"defined": "I80F48"
}
},
{
"name": "platformLiquidationFee",
"docs": [
"Additional to liquidation_fee, but goes to the group owner instead of the liqor"
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collectedLiquidationFees",
"docs": [
"Platform fees that were collected during liquidation (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1968
1920
]
}
}
@ -7456,10 +7634,16 @@
},
{
"name": "fastListingsInInterval",
"docs": [
"Number of fast listings that happened this interval"
],
"type": "u16"
},
{
"name": "allowedFastListingsPerInterval",
"docs": [
"Number of fast listings that are allowed per interval"
],
"type": "u16"
},
{
@ -7739,12 +7923,16 @@
"name": "registrationTime",
"type": "u64"
},
{
"name": "fallbackOracle",
"type": "publicKey"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
2560
2528
]
}
}
@ -8384,12 +8572,33 @@
"name": "feesWithdrawn",
"type": "u64"
},
{
"name": "platformLiquidationFee",
"docs": [
"Additional to liquidation_fee, but goes to the group owner instead of the liqor"
],
"type": {
"defined": "I80F48"
}
},
{
"name": "accruedLiquidationFees",
"docs": [
"Platform fees that were accrued during liquidation (in native tokens)",
"",
"These fees are also added to fees_accrued, this is just for bookkeeping the total",
"liquidation fees that happened. So never decreases (different to fees_accrued)."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1880
1848
]
}
}
@ -9125,36 +9334,44 @@
"type": "f64"
},
{
"name": "realizedTradePnlNative",
"name": "deprecatedRealizedTradePnlNative",
"docs": [
"Amount of pnl that was realized by bringing the base position closer to 0.",
"",
"The settlement of this type of pnl is limited by settle_pnl_limit_realized_trade.",
"Settling pnl reduces this value once other_pnl below is exhausted."
"Deprecated field: Amount of pnl that was realized by bringing the base position closer to 0."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "realizedOtherPnlNative",
"name": "oneshotSettlePnlAllowance",
"docs": [
"Amount of pnl realized from fees, funding and liquidation.",
"Amount of pnl that can be settled once.",
"",
"This type of realized pnl is always settleable.",
"Settling pnl reduces this value first."
"- The value is signed: a negative number means negative pnl can be settled.",
"- A settlement in the right direction will decrease this amount.",
"",
"Typically added for fees, funding and liquidation."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "settlePnlLimitRealizedTrade",
"name": "recurringSettlePnlAllowance",
"docs": [
"Settle limit contribution from realized pnl.",
"Amount of pnl that can be settled in each settle window.",
"",
"Every time pnl is realized, this is increased by a fraction of the stable",
"value of the realization. It magnitude decreases when realized pnl drops below its value."
"- Unsigned, the settlement can happen in both directions. Value is >= 0.",
"- Previously stored a similar value that was signed, so in migration cases",
"this value can be negative and should be .abs()ed.",
"- If this value exceeds the current stable-upnl, it should be decreased,",
"see apply_recurring_settle_pnl_allowance_constraint()",
"",
"When the base position is reduced, the settle limit contribution from the reduced",
"base position is materialized into this value. When the base position increases,",
"some of the allowance is taken away.",
"",
"This also gets increased when a liquidator takes over pnl."
],
"type": "i64"
},
@ -9224,12 +9441,16 @@
"name": "id",
"type": "u128"
},
{
"name": "quantity",
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
56
]
}
}
@ -9784,13 +10005,8 @@
"type": "u64"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
16
]
}
"name": "makerOrderId",
"type": "u128"
},
{
"name": "price",
@ -9866,12 +10082,16 @@
"name": "quantity",
"type": "i64"
},
{
"name": "orderId",
"type": "u128"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
144
128
]
}
}
@ -10646,6 +10866,9 @@
},
{
"name": "SwitchboardV2"
},
{
"name": "OrcaCLMM"
}
]
}
@ -11823,6 +12046,71 @@
}
]
},
{
"name": "TokenLiqWithTokenLogV2",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "assetTokenIndex",
"type": "u16",
"index": false
},
{
"name": "liabTokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetTransferFromLiqee",
"type": "i128",
"index": false
},
{
"name": "assetTransferToLiqor",
"type": "i128",
"index": false
},
{
"name": "assetLiquidationFee",
"type": "i128",
"index": false
},
{
"name": "liabTransfer",
"type": "i128",
"index": false
},
{
"name": "assetPrice",
"type": "i128",
"index": false
},
{
"name": "liabPrice",
"type": "i128",
"index": false
},
{
"name": "bankruptcy",
"type": "bool",
"index": false
}
]
},
{
"name": "Serum3OpenOrdersBalanceLog",
"fields": [
@ -12174,6 +12462,46 @@
}
]
},
{
"name": "TokenMetaDataLogV2",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mint",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "mintDecimals",
"type": "u8",
"index": false
},
{
"name": "oracle",
"type": "publicKey",
"index": false
},
{
"name": "fallbackOracle",
"type": "publicKey",
"index": false
},
{
"name": "mintInfo",
"type": "publicKey",
"index": false
}
]
},
{
"name": "PerpMarketMetaDataLog",
"fields": [
@ -12304,6 +12632,131 @@
}
]
},
{
"name": "PerpLiqBaseOrPositivePnlLogV2",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "baseTransferLiqee",
"type": "i64",
"index": false
},
{
"name": "quoteTransferLiqee",
"type": "i128",
"index": false
},
{
"name": "quoteTransferLiqor",
"type": "i128",
"index": false
},
{
"name": "quotePlatformFee",
"type": "i128",
"index": false
},
{
"name": "pnlTransfer",
"type": "i128",
"index": false
},
{
"name": "pnlSettleLimitTransfer",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpLiqBaseOrPositivePnlLogV3",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "baseTransferLiqee",
"type": "i64",
"index": false
},
{
"name": "quoteTransferLiqee",
"type": "i128",
"index": false
},
{
"name": "quoteTransferLiqor",
"type": "i128",
"index": false
},
{
"name": "quotePlatformFee",
"type": "i128",
"index": false
},
{
"name": "pnlTransfer",
"type": "i128",
"index": false
},
{
"name": "pnlSettleLimitTransferRecurring",
"type": "i64",
"index": false
},
{
"name": "pnlSettleLimitTransferOneshot",
"type": "i64",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpLiqBankruptcyLog",
"fields": [
@ -12659,6 +13112,71 @@
}
]
},
{
"name": "TokenForceCloseBorrowsWithTokenLogV2",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "assetTokenIndex",
"type": "u16",
"index": false
},
{
"name": "liabTokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetTransferFromLiqee",
"type": "i128",
"index": false
},
{
"name": "assetTransferToLiqor",
"type": "i128",
"index": false
},
{
"name": "assetLiquidationFee",
"type": "i128",
"index": false
},
{
"name": "liabTransfer",
"type": "i128",
"index": false
},
{
"name": "assetPrice",
"type": "i128",
"index": false
},
{
"name": "liabPrice",
"type": "i128",
"index": false
},
{
"name": "feeFactor",
"type": "i128",
"index": false
}
]
},
{
"name": "TokenConditionalSwapCreateLog",
"fields": [
@ -13545,6 +14063,36 @@
"code": 6062,
"name": "BankDepositLimit",
"msg": "deposit crosses the token's deposit limit"
},
{
"code": 6063,
"name": "DelegateWithdrawOnlyToOwnerAta",
"msg": "delegates can only withdraw to the owner's associated token account"
},
{
"code": 6064,
"name": "DelegateWithdrawMustClosePosition",
"msg": "delegates can only withdraw if they close the token position"
},
{
"code": 6065,
"name": "DelegateWithdrawSmall",
"msg": "delegates can only withdraw small amounts"
},
{
"code": 6066,
"name": "InvalidCLMMOracle",
"msg": "The provided CLMM oracle is not valid"
},
{
"code": 6067,
"name": "InvalidFeedForCLMMOracle",
"msg": "invalid usdc/usd feed provided for the CLMM oracle"
},
{
"code": 6068,
"name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
}
]
}

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.21.2"
version = "0.22.0"
description = "Created with Anchor"
edition = "2021"
@ -14,11 +14,12 @@ no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
default = ["custom-heap"]
test-bpf = ["client"]
client = ["solana-sdk", "no-entrypoint"]
# Enables GPL-licensed parts of the code. See LICENSE file.
enable-gpl = ["openbook-v2/enable-gpl"]
custom-heap = []
[dependencies]
# todo: when to fix, when to use caret? need a regular chore to bump dependencies

View File

@ -12,8 +12,6 @@ pub struct StubOracleCreate<'info> {
#[account(
init,
seeds = [b"StubOracle".as_ref(), group.key().as_ref(), mint.key().as_ref()],
bump,
payer = payer,
space = 8 + std::mem::size_of::<StubOracle>(),
)]

View File

@ -21,4 +21,9 @@ pub struct TokenEdit<'info> {
///
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
/// The fallback oracle account is optional and only used when set_fallback_oracle is true.
///
/// CHECK: The fallback oracle can be one of several different account types
pub fallback_oracle: UncheckedAccount<'info>,
}

View File

@ -51,6 +51,9 @@ pub struct TokenRegister<'info> {
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
/// CHECK: The oracle can be one of several different account types
pub fallback_oracle: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,

View File

@ -51,6 +51,9 @@ pub struct TokenRegisterTrustless<'info> {
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
/// CHECK: The oracle can be one of several different account types
pub fallback_oracle: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,

View File

@ -16,8 +16,12 @@ pub struct TokenWithdraw<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen,
// Delegates are allowed to call this instruction, but only with significant constraints,
// like "must close position", "tiny amount" and "token_account is a owner ATA"
// which allows delegated liquidators to close their token positions. See #1
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub owner: Signer<'info>,

View File

@ -0,0 +1,85 @@
#![allow(dead_code)]
use std::alloc::{GlobalAlloc, Layout};
/// The end of the region where heap space may be reserved for the program.
///
/// The actual size of the heap is currently not available at runtime.
pub const HEAP_END_ADDRESS: usize = 0x400000000;
#[cfg(not(feature = "no-entrypoint"))]
#[global_allocator]
pub static ALLOCATOR: BumpAllocator = BumpAllocator {};
pub fn heap_used() -> usize {
#[cfg(not(feature = "no-entrypoint"))]
return ALLOCATOR.used();
#[cfg(feature = "no-entrypoint")]
return 0;
}
/// Custom bump allocator for on-chain operations
///
/// The default allocator is also a bump one, but grows from a fixed
/// HEAP_START + 32kb downwards and has no way of making use of extra
/// heap space requested for the transaction.
///
/// This implementation starts at HEAP_START and grows upward, producing
/// a segfault once out of available heap memory.
pub struct BumpAllocator {}
unsafe impl GlobalAlloc for BumpAllocator {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let heap_start = solana_program::entrypoint::HEAP_START_ADDRESS as usize;
let pos_ptr = heap_start as *mut usize;
let mut pos = *pos_ptr;
if pos == 0 {
// First time, override the current position to be just past the location
// where the current heap position is stored.
pos = heap_start + 8;
}
// The result address needs to be aligned to layout.align(),
// which is guaranteed to be a power of two.
// Find the first address >=pos that has the required alignment.
// Wrapping ops are used for performance.
let mask = layout.align().wrapping_sub(1);
let begin = pos.wrapping_add(mask) & (!mask);
// Update allocator state
let end = begin.checked_add(layout.size()).unwrap();
*pos_ptr = end;
// Ensure huge allocations can't escape the dedicated heap memory region
assert!(end < HEAP_END_ADDRESS);
// Write a byte to trigger heap overflow errors early
let end_ptr = end as *mut u8;
*end_ptr = 0;
begin as *mut u8
}
#[inline]
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
// I'm a bump allocator, I don't free
}
}
impl BumpAllocator {
#[inline]
pub fn used(&self) -> usize {
let heap_start = solana_program::entrypoint::HEAP_START_ADDRESS as usize;
unsafe {
let pos_ptr = heap_start as *mut usize;
let pos = *pos_ptr;
if pos == 0 {
return 0;
}
return pos - heap_start;
}
}
}

View File

@ -131,6 +131,18 @@ pub enum MangoError {
Serum3PriceBandExceeded,
#[msg("deposit crosses the token's deposit limit")]
BankDepositLimit,
#[msg("delegates can only withdraw to the owner's associated token account")]
DelegateWithdrawOnlyToOwnerAta,
#[msg("delegates can only withdraw if they close the token position")]
DelegateWithdrawMustClosePosition,
#[msg("delegates can only withdraw small amounts")]
DelegateWithdrawSmall,
#[msg("The provided CLMM oracle is not valid")]
InvalidCLMMOracle,
#[msg("invalid usdc/usd feed provided for the CLMM oracle")]
InvalidFeedForCLMMOracle,
#[msg("Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)")]
MissingFeedForCLMMOracle,
}
impl MangoError {

View File

@ -10,6 +10,9 @@ use std::collections::HashMap;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::serum3_cpi;
use crate::state::pyth_mainnet_sol_oracle;
use crate::state::pyth_mainnet_usdc_oracle;
use crate::state::OracleAccountInfos;
use crate::state::{Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, TokenIndex};
/// This trait abstracts how to find accounts needed for the health computation.
@ -47,6 +50,7 @@ pub trait AccountRetriever {
/// 3. PerpMarket accounts, in the order of account.perps.iter_active_accounts()
/// 4. PerpMarket oracle accounts, in the order of the perp market accounts
/// 5. serum3 OpenOrders accounts, in the order of account.serum3.iter_active()
/// 6. fallback oracle accounts, order and existence of accounts is not guaranteed
pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub ais: Vec<T>,
pub n_banks: usize,
@ -54,6 +58,9 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub begin_perp: usize,
pub begin_serum3: usize,
pub staleness_slot: Option<u64>,
pub begin_fallback_oracles: usize,
pub usd_oracle_index: Option<usize>,
pub sol_oracle_index: Option<usize>,
}
pub fn new_fixed_order_account_retriever<'a, 'info>(
@ -66,11 +73,17 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
let expected_ais = active_token_len * 2 // banks + oracles
+ active_perp_len * 2 // PerpMarkets + Oracles
+ active_serum3_len; // open_orders
require_msg_typed!(ais.len() == expected_ais, MangoError::InvalidHealthAccountCount,
require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount,
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
ais.len(), expected_ais,
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len
);
let usd_oracle_index = ais[..]
.iter()
.position(|o| o.key == &pyth_mainnet_usdc_oracle::ID);
let sol_oracle_index = ais[..]
.iter()
.position(|o| o.key == &pyth_mainnet_sol_oracle::ID);
Ok(FixedOrderAccountRetriever {
ais: AccountInfoRef::borrow_slice(ais)?,
@ -79,6 +92,9 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: Some(Clock::get()?.slot),
begin_fallback_oracles: expected_ais,
usd_oracle_index,
sol_oracle_index,
})
}
@ -103,14 +119,29 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
Ok(market)
}
fn oracle_price_bank(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
let oracle = &self.ais[account_index];
bank.oracle_price(oracle, self.staleness_slot)
}
fn oracle_price_perp(&self, account_index: usize, perp_market: &PerpMarket) -> Result<I80F48> {
let oracle = &self.ais[account_index];
perp_market.oracle_price(oracle, self.staleness_slot)
let oracle_acc_infos = OracleAccountInfos::from_reader(oracle);
perp_market.oracle_price(&oracle_acc_infos, self.staleness_slot)
}
#[inline(always)]
fn create_oracle_infos(
&self,
oracle_index: usize,
fallback_key: &Pubkey,
) -> OracleAccountInfos<T> {
let oracle = &self.ais[oracle_index];
let fallback_opt = self.ais[self.begin_fallback_oracles..]
.iter()
.find(|ai| ai.key() == fallback_key);
OracleAccountInfos {
oracle,
fallback_opt,
usd_opt: self.usd_oracle_index.map(|i| &self.ais[i]),
sol_opt: self.sol_oracle_index.map(|i| &self.ais[i]),
}
}
}
@ -134,7 +165,9 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
})?;
let oracle_index = self.n_banks + active_token_position_index;
let oracle_price = self.oracle_price_bank(oracle_index, bank).with_context(|| {
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(|| {
format!(
"getting oracle for bank with health account index {} and token index {}, passed account {}",
bank_account_index,
@ -196,8 +229,13 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
pub struct ScannedBanksAndOracles<'a, 'info> {
banks: Vec<AccountInfoRefMut<'a, 'info>>,
oracles: Vec<AccountInfoRef<'a, 'info>>,
fallback_oracles: Vec<AccountInfoRef<'a, 'info>>,
index_map: HashMap<TokenIndex, usize>,
staleness_slot: Option<u64>,
/// index in fallback_oracles
usd_oracle_index: Option<usize>,
/// index in fallback_oracles
sol_oracle_index: Option<usize>,
}
impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
@ -220,9 +258,13 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
) -> Result<(&mut Bank, I80F48, Option<(&mut Bank, I80F48)>)> {
if token_index1 == token_index2 {
let index = self.bank_index(token_index1)?;
let price = {
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
let oracle_acc_infos = self.create_oracle_infos(index, &bank.fallback_oracle);
bank.oracle_price(&oracle_acc_infos, self.staleness_slot)?
};
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
let price = bank.oracle_price(oracle, self.staleness_slot)?;
return Ok((bank, price, None));
}
let index1 = self.bank_index(token_index1)?;
@ -233,15 +275,21 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
(index2, index1, true)
};
let (price1, price2) = {
let bank1 = self.banks[first].load_fully_unchecked::<Bank>()?;
let bank2 = self.banks[second].load_fully_unchecked::<Bank>()?;
let oracle_infos_1 = self.create_oracle_infos(first, &bank1.fallback_oracle);
let oracle_infos_2 = self.create_oracle_infos(second, &bank2.fallback_oracle);
let price1 = bank1.oracle_price(&oracle_infos_1, self.staleness_slot)?;
let price2 = bank2.oracle_price(&oracle_infos_2, self.staleness_slot)?;
(price1, price2)
};
// split_at_mut after the first bank and after the second bank
let (first_bank_part, second_bank_part) = self.banks.split_at_mut(first + 1);
let bank1 = first_bank_part[first].load_mut_fully_unchecked::<Bank>()?;
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
let oracle1 = &self.oracles[first];
let oracle2 = &self.oracles[second];
let price1 = bank1.oracle_price(oracle1, self.staleness_slot)?;
let price2 = bank2.oracle_price(oracle2, self.staleness_slot)?;
if swap {
Ok((bank2, price2, Some((bank1, price1))))
} else {
@ -253,10 +301,33 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
let index = self.bank_index(token_index)?;
// The account was already loaded successfully during construction
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
let price = bank.oracle_price(oracle, self.staleness_slot)?;
let oracle_acc_infos = self.create_oracle_infos(index, &bank.fallback_oracle);
let price = bank.oracle_price(&oracle_acc_infos, self.staleness_slot)?;
Ok((bank, price))
}
#[inline(always)]
fn create_oracle_infos(
&self,
oracle_index: usize,
fallback_key: &Pubkey,
) -> OracleAccountInfos<AccountInfoRef> {
let oracle = &self.oracles[oracle_index];
let fallback_opt = if fallback_key == &Pubkey::default() {
None
} else {
self.fallback_oracles
.iter()
.find(|ai| ai.key == fallback_key)
};
OracleAccountInfos {
oracle,
fallback_opt,
usd_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]),
sol_opt: self.sol_oracle_index.map(|i| &self.fallback_oracles[i]),
}
}
}
/// Takes a list of account infos containing
@ -265,6 +336,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
/// - an unknown number of PerpMarket accounts
/// - the same number of oracles in the same order as the perp markets
/// - an unknown number of serum3 OpenOrders accounts
/// - an unknown number of fallback oracle accounts
/// and retrieves accounts needed for the health computation by doing a linear
/// scan for each request.
pub struct ScanningAccountRetriever<'a, 'info> {
@ -350,17 +422,34 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let n_perps = perp_index_map.len();
let perp_oracles_start = perps_start + n_perps;
let serum3_start = perp_oracles_start + n_perps;
let n_serum3 = ais[serum3_start..]
.iter()
.take_while(|x| {
x.data_len() == std::mem::size_of::<serum_dex::state::OpenOrders>() + 12
&& serum3_cpi::has_serum_header(&x.data.borrow())
})
.count();
let fallback_oracles_start = serum3_start + n_serum3;
let usd_oracle_index = ais[fallback_oracles_start..]
.iter()
.position(|o| o.key == &pyth_mainnet_usdc_oracle::ID);
let sol_oracle_index = ais[fallback_oracles_start..]
.iter()
.position(|o| o.key == &pyth_mainnet_sol_oracle::ID);
Ok(Self {
banks_and_oracles: ScannedBanksAndOracles {
banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?,
oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?,
fallback_oracles: AccountInfoRef::borrow_slice(&ais[fallback_oracles_start..])?,
index_map: token_index_map,
staleness_slot,
usd_oracle_index,
sol_oracle_index,
},
perp_markets: AccountInfoRef::borrow_slice(&ais[perps_start..perp_oracles_start])?,
perp_oracles: AccountInfoRef::borrow_slice(&ais[perp_oracles_start..serum3_start])?,
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?,
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..fallback_oracles_start])?,
perp_index_map,
})
}
@ -395,7 +484,9 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
// The account was already loaded successfully during construction
let perp_market = self.perp_markets[index].load_fully_unchecked::<PerpMarket>()?;
let oracle_acc = &self.perp_oracles[index];
let price = perp_market.oracle_price(oracle_acc, self.banks_and_oracles.staleness_slot)?;
let oracle_acc_infos = OracleAccountInfos::from_reader(oracle_acc);
let price =
perp_market.oracle_price(&oracle_acc_infos, self.banks_and_oracles.staleness_slot)?;
Ok((perp_market, price))
}

View File

@ -38,14 +38,16 @@ pub fn account_buyback_fees_with_mngo(
let now_ts = clock.unix_timestamp.try_into().unwrap();
let slot = clock.slot;
let mngo_oracle_ref = &AccountInfoRef::borrow(&ctx.accounts.mngo_oracle.as_ref())?;
let mngo_oracle_price = mngo_bank.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.mngo_oracle.as_ref())?,
&OracleAccountInfos::from_reader(mngo_oracle_ref),
Some(slot),
)?;
let mngo_asset_price = mngo_oracle_price.min(mngo_bank.stable_price());
let fees_oracle_ref = &AccountInfoRef::borrow(&ctx.accounts.fees_oracle.as_ref())?;
let fees_oracle_price = fees_bank.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.fees_oracle.as_ref())?,
&OracleAccountInfos::from_reader(fees_oracle_ref),
Some(slot),
)?;
let fees_liab_price = fees_oracle_price.max(fees_bank.stable_price());

View File

@ -37,6 +37,7 @@ pub use perp_settle_pnl::*;
pub use perp_update_funding::*;
pub use serum3_cancel_all_orders::*;
pub use serum3_cancel_order::*;
pub use serum3_cancel_order_by_client_order_id::*;
pub use serum3_close_open_orders::*;
pub use serum3_create_open_orders::*;
pub use serum3_deregister_market::*;
@ -103,6 +104,7 @@ mod perp_settle_pnl;
mod perp_update_funding;
mod serum3_cancel_all_orders;
mod serum3_cancel_order;
mod serum3_cancel_order_by_client_order_id;
mod serum3_close_open_orders;
mod serum3_create_open_orders;
mod serum3_deregister_market;

View File

@ -18,7 +18,13 @@ pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> R
asks: ctx.accounts.asks.load_mut()?,
};
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
book.cancel_all_orders(
&mut account.borrow_mut(),
ctx.accounts.account.as_ref().key,
&mut perp_market,
limit,
None,
)?;
Ok(())
}

View File

@ -24,6 +24,7 @@ pub fn perp_cancel_all_orders_by_side(
book.cancel_all_orders(
&mut account.borrow_mut(),
ctx.accounts.account.as_ref().key,
&mut perp_market,
limit,
side_option,

View File

@ -18,19 +18,20 @@ pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: u128) -> Resul
asks: ctx.accounts.asks.load_mut()?,
};
let oo = account
let (slot, _) = account
.perp_find_order_with_order_id(perp_market.perp_market_index, order_id)
.ok_or_else(|| {
error_msg!("could not find perp order with id {order_id} in user account")
error_msg_typed!(
MangoError::PerpOrderIdNotFound,
"could not find perp order with id {order_id} in user account"
)
})?;
let order_id = oo.id;
let order_side_and_tree = oo.side_and_tree();
book.cancel_order(
book.cancel_order_by_slot(
&mut account.borrow_mut(),
order_id,
order_side_and_tree,
Some(ctx.accounts.account.key()),
ctx.accounts.account.as_ref().key,
slot,
perp_market.perp_market_index,
)?;
Ok(())

View File

@ -21,21 +21,20 @@ pub fn perp_cancel_order_by_client_order_id(
asks: ctx.accounts.asks.load_mut()?,
};
let oo = account
let (slot, _) = account
.perp_find_order_with_client_order_id(perp_market.perp_market_index, client_order_id)
.ok_or_else(|| {
error_msg!(
error_msg_typed!(
MangoError::PerpOrderIdNotFound,
"could not find perp order with client order id {client_order_id} in user account"
)
})?;
let order_id = oo.id;
let order_side_and_tree = oo.side_and_tree();
book.cancel_order(
book.cancel_order_by_slot(
&mut account.borrow_mut(),
order_id,
order_side_and_tree,
Some(ctx.accounts.account.key()),
ctx.accounts.account.as_ref().key,
slot,
perp_market.perp_market_index,
)?;
Ok(())

View File

@ -74,40 +74,37 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
group,
event_queue
);
let before_pnl = maker_taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
maker_taker.execute_perp_maker(
let maker_realized_pnl = maker_taker.execute_perp_maker(
perp_market_index,
&mut perp_market,
fill,
&group,
)?;
maker_taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?;
let taker_realized_pnl = maker_taker.execute_perp_taker(
perp_market_index,
&mut perp_market,
fill,
)?;
emit_perp_balances(
group_key,
fill.maker,
maker_taker.perp_position(perp_market_index).unwrap(),
&perp_market,
);
let after_pnl = maker_taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let closed_pnl = after_pnl - before_pnl;
let closed_pnl = maker_realized_pnl + taker_realized_pnl;
(closed_pnl, closed_pnl)
} else {
load_mango_account!(maker, fill.maker, mango_account_ais, group, event_queue);
load_mango_account!(taker, fill.taker, mango_account_ais, group, event_queue);
let maker_before_pnl = maker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let taker_before_pnl = taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
maker.execute_perp_maker(perp_market_index, &mut perp_market, fill, &group)?;
taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?;
let maker_realized_pnl = maker.execute_perp_maker(
perp_market_index,
&mut perp_market,
fill,
&group,
)?;
let taker_realized_pnl =
taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?;
emit_perp_balances(
group_key,
fill.maker,
@ -120,16 +117,8 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
taker.perp_position(perp_market_index).unwrap(),
&perp_market,
);
let maker_after_pnl = maker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let taker_after_pnl = taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let maker_closed_pnl = maker_after_pnl - maker_before_pnl;
let taker_closed_pnl = taker_after_pnl - taker_before_pnl;
(maker_closed_pnl, taker_closed_pnl)
(maker_realized_pnl, taker_realized_pnl)
};
emit_stack(FillLogV3 {
mango_group: group_key,
@ -155,7 +144,13 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
EventType::Out => {
let out: &OutEvent = cast_ref(event);
load_mango_account!(owner, out.owner, mango_account_ais, group, event_queue);
owner.remove_perp_order(out.owner_slot as usize, out.quantity)?;
owner.execute_perp_out_event(
perp_market_index,
out.side(),
out.owner_slot as usize,
out.quantity,
out.order_id,
)?;
}
EventType::Liquidate => {
// This is purely for record keeping. Can be removed if program logs are superior

View File

@ -39,6 +39,7 @@ pub fn perp_create_market(
settle_pnl_limit_factor: f32,
settle_pnl_limit_window_size_ts: u64,
positive_pnl_liquidation_fee: f32,
platform_liquidation_fee: f32,
) -> Result<()> {
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
@ -92,11 +93,14 @@ pub fn perp_create_market(
init_overall_asset_weight: I80F48::from_num(init_overall_asset_weight),
positive_pnl_liquidation_fee: I80F48::from_num(positive_pnl_liquidation_fee),
fees_withdrawn: 0,
reserved: [0; 1880],
platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee),
accrued_liquidation_fees: I80F48::ZERO,
reserved: [0; 1848],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) =
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)
{
perp_market
.stable_price_model

View File

@ -39,6 +39,7 @@ pub fn perp_edit_market(
positive_pnl_liquidation_fee_opt: Option<f32>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
platform_liquidation_fee_opt: Option<f32>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -65,8 +66,9 @@ pub fn perp_edit_market(
if reset_stable_price {
msg!("Stable price reset");
require_keys_eq!(perp_market.oracle, ctx.accounts.oracle.key());
let oracle_price = perp_market
.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price =
perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)?;
perp_market.stable_price_model.reset_to_price(
oracle_price.to_num(),
Clock::get()?.unix_timestamp.try_into().unwrap(),
@ -344,6 +346,16 @@ pub fn perp_edit_market(
require_group_admin = true;
};
if let Some(platform_liquidation_fee) = platform_liquidation_fee_opt {
msg!(
"Platform liquidation fee: old - {:?}, new - {:?}",
perp_market.platform_liquidation_fee,
platform_liquidation_fee
);
perp_market.platform_liquidation_fee = I80F48::from_num(platform_liquidation_fee);
require_group_admin = true;
};
// account constraint #1
if require_group_admin {
require!(

View File

@ -34,10 +34,9 @@ pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result
.min(account_b_perp_position.base_position_lots().abs())
.max(0);
let now_slot = Clock::get()?.slot;
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
)?;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price =
perp_market.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
let quote_transfer = I80F48::from(base_transfer * perp_market.base_lot_size) * oracle_price;
account_a_perp_position.record_trade(&mut perp_market, -base_transfer, quote_transfer);

View File

@ -8,7 +8,7 @@ use crate::health::*;
use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLog, TokenBalanceLog};
use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLogV3, TokenBalanceLog};
/// This instruction deals with increasing health by:
/// - reducing the liqee's base position
@ -69,8 +69,9 @@ pub fn perp_liq_base_or_positive_pnl(
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
// Get oracle price for market. Price is validated inside
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
None, // checked in health
)?;
@ -93,18 +94,25 @@ pub fn perp_liq_base_or_positive_pnl(
//
// Perform the liquidation
//
let (base_transfer, quote_transfer, pnl_transfer, pnl_settle_limit_transfer) =
liquidation_action(
&mut perp_market,
&mut settle_bank,
&mut liqor.borrow_mut(),
&mut liqee.borrow_mut(),
&mut liqee_health_cache,
liqee_liq_end_health,
now_ts,
max_base_transfer,
max_pnl_transfer,
)?;
let (
base_transfer,
quote_transfer_liqee,
quote_transfer_liqor,
platform_fee,
pnl_transfer,
pnl_settle_limit_transfer_recurring,
pnl_settle_limit_transfer_oneshot,
) = liquidation_action(
&mut perp_market,
&mut settle_bank,
&mut liqor.borrow_mut(),
&mut liqee.borrow_mut(),
&mut liqee_health_cache,
liqee_liq_end_health,
now_ts,
max_base_transfer,
max_pnl_transfer,
)?;
//
// Log changes
@ -151,15 +159,18 @@ pub fn perp_liq_base_or_positive_pnl(
}
if base_transfer != 0 || pnl_transfer != 0 {
emit_stack(PerpLiqBaseOrPositivePnlLog {
emit_stack(PerpLiqBaseOrPositivePnlLogV3 {
mango_group: ctx.accounts.group.key(),
perp_market_index: perp_market.perp_market_index,
liqor: ctx.accounts.liqor.key(),
liqee: ctx.accounts.liqee.key(),
base_transfer,
quote_transfer: quote_transfer.to_bits(),
base_transfer_liqee: base_transfer,
quote_transfer_liqee: quote_transfer_liqee.to_bits(),
quote_transfer_liqor: quote_transfer_liqor.to_bits(),
quote_platform_fee: platform_fee.to_bits(),
pnl_transfer: pnl_transfer.to_bits(),
pnl_settle_limit_transfer: pnl_settle_limit_transfer.to_bits(),
pnl_settle_limit_transfer_recurring,
pnl_settle_limit_transfer_oneshot,
price: oracle_price.to_bits(),
});
}
@ -206,7 +217,7 @@ pub(crate) fn liquidation_action(
now_ts: u64,
max_base_transfer: i64,
max_pnl_transfer: u64,
) -> Result<(i64, I80F48, I80F48, I80F48)> {
) -> Result<(i64, I80F48, I80F48, I80F48, I80F48, i64, i64)> {
let liq_end_type = HealthType::LiquidationEnd;
let perp_market_index = perp_market.perp_market_index;
@ -278,7 +289,8 @@ pub(crate) fn liquidation_action(
let direction: i64;
// Either 1+fee or 1-fee, depending on direction.
let base_fee_factor;
let base_fee_factor_liqor;
let base_fee_factor_all;
if liqee_base_lots > 0 {
require_msg!(
@ -287,11 +299,12 @@ pub(crate) fn liquidation_action(
);
// the health_unsettled_pnl gets reduced by `base * base_price * perp_init_asset_weight`
// and increased by `base * base_price * (1 - liq_fee)`
// and increased by `base * base_price * (1 - liq_fees)`
direction = -1;
base_fee_factor = I80F48::ONE - perp_market.base_liquidation_fee;
base_fee_factor_liqor = I80F48::ONE - perp_market.base_liquidation_fee;
base_fee_factor_all = base_fee_factor_liqor - perp_market.platform_liquidation_fee;
uhupnl_per_lot =
oracle_price_per_lot * (-perp_market.init_base_asset_weight + base_fee_factor);
oracle_price_per_lot * (-perp_market.init_base_asset_weight + base_fee_factor_all);
} else {
// liqee_base_lots <= 0
require_msg!(
@ -300,11 +313,12 @@ pub(crate) fn liquidation_action(
);
// health gets increased by `base * base_price * perp_init_liab_weight`
// and reduced by `base * base_price * (1 + liq_fee)`
// and reduced by `base * base_price * (1 + liq_fees)`
direction = 1;
base_fee_factor = I80F48::ONE + perp_market.base_liquidation_fee;
base_fee_factor_liqor = I80F48::ONE + perp_market.base_liquidation_fee;
base_fee_factor_all = base_fee_factor_liqor + perp_market.platform_liquidation_fee;
uhupnl_per_lot =
oracle_price_per_lot * (perp_market.init_base_liab_weight - base_fee_factor);
oracle_price_per_lot * (perp_market.init_base_liab_weight - base_fee_factor_all);
};
assert!(uhupnl_per_lot > 0);
@ -536,22 +550,35 @@ pub(crate) fn liquidation_action(
//
assert!(base_reduction <= liqee_base_lots.abs());
let base_transfer = direction * base_reduction;
let quote_transfer = -I80F48::from(base_transfer) * oracle_price_per_lot * base_fee_factor;
let quote_transfer_base = -I80F48::from(base_transfer) * oracle_price_per_lot;
let quote_transfer_liqee = quote_transfer_base * base_fee_factor_all;
let quote_transfer_liqor = -quote_transfer_base * base_fee_factor_liqor;
if base_transfer != 0 {
msg!(
"transfering: {} base lots and {} quote",
base_transfer,
quote_transfer
quote_transfer_liqee
);
liqee_perp_position.record_trade(perp_market, base_transfer, quote_transfer);
liqor_perp_position.record_trade(perp_market, -base_transfer, -quote_transfer);
liqee_perp_position.record_trade(perp_market, base_transfer, quote_transfer_liqee);
liqor_perp_position.record_trade(perp_market, -base_transfer, quote_transfer_liqor);
}
// We know that this is positive:
// liq a long: base_transfer < 0, quote_transfer_base > 0, base_fee_factor < 1
// and -q_t_liqor >= q_t_liqee (both sides positive; take more from the liqor than we give to the liqee)
// liq a short: base_transfer > 0, quote_transfer_base < 0, base_fee_factor > 1
// and -q_t_liqor >= q_t_liqee (both sides negative; we take more from the liqee than we give to the liqor)
let platform_fee = (-quote_transfer_liqor - quote_transfer_liqee).max(I80F48::ZERO);
perp_market.fees_accrued += platform_fee;
perp_market.accrued_liquidation_fees += platform_fee;
//
// Let the liqor take over positive pnl until the account health is positive,
// but only while the health_unsettled_pnl is positive (otherwise it would decrease liqee health!)
//
let limit_transfer = if pnl_transfer > 0 {
let limit_transfer_recurring: i64;
let limit_transfer_oneshot: i64;
if pnl_transfer > 0 {
// Allow taking over *more* than the liqee_positive_settle_limit. In exchange, the liqor
// also can't settle fully immediately and just takes over a fractional chunk of the limit.
//
@ -559,23 +586,46 @@ pub(crate) fn liquidation_action(
// base position to zero and would need to deal with that in bankruptcy. Also, the settle
// limit changes with the base position price, so it'd be hard to say when this liquidation
// step is done.
let limit_transfer = {
{
// take care, liqee_limit may be i64::MAX
let liqee_limit: i128 = liqee_positive_settle_limit.into();
let liqee_oneshot_positive = liqee_perp_position
.oneshot_settle_pnl_allowance
.ceil()
.to_num::<i128>()
.max(0);
let liqee_recurring = (liqee_limit - liqee_oneshot_positive).max(0);
let liqee_pnl = liqee_perp_position
.unsettled_pnl(perp_market, oracle_price)?
.max(I80F48::ONE);
.ceil()
.to_num::<i128>()
.max(1);
let settle = pnl_transfer.floor().to_num::<i128>();
let total = liqee_pnl.ceil().to_num::<i128>();
let liqor_limit: i64 = (liqee_limit * settle / total).try_into().unwrap();
I80F48::from(liqor_limit).min(pnl_transfer).max(I80F48::ONE)
let total = liqee_pnl.max(settle);
let transfer_recurring: i64 = (liqee_recurring * settle / total).try_into().unwrap();
let transfer_oneshot: i64 = (liqee_oneshot_positive * settle / total)
.try_into()
.unwrap();
// never transfer more than pnl_transfer rounded up
// and transfer at least 1, to compensate for rounding down `settle` and int div
let max_transfer = pnl_transfer.ceil().to_num::<i64>();
limit_transfer_recurring = transfer_recurring.min(max_transfer).max(1);
// make is so the sum of recurring and oneshot doesn't exceed max_transfer
limit_transfer_oneshot = transfer_oneshot
.min(max_transfer - limit_transfer_recurring)
.max(0);
};
// The liqor pays less than the full amount to receive the positive pnl
let token_transfer = pnl_transfer * spot_gain_per_settled;
liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer);
liqee_perp_position.record_settle(pnl_transfer);
liqor_perp_position.record_liquidation_pnl_takeover(
pnl_transfer,
limit_transfer_recurring,
limit_transfer_oneshot,
);
liqee_perp_position.record_settle(pnl_transfer, &perp_market);
// Update the accounts' perp_spot_transfer statistics.
let transfer_i64 = token_transfer.round_to_zero().to_num::<i64>();
@ -592,21 +642,29 @@ pub(crate) fn liquidation_action(
liqee_health_cache.adjust_token_balance(&settle_bank, token_transfer)?;
msg!(
"pnl {} was transferred to liqor for quote {} with settle limit {}",
"pnl {} was transferred to liqor for quote {} with settle limit {} recurring/{} oneshot",
pnl_transfer,
token_transfer,
limit_transfer
limit_transfer_recurring,
limit_transfer_oneshot,
);
limit_transfer
} else {
I80F48::ZERO
limit_transfer_oneshot = 0;
limit_transfer_recurring = 0;
};
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
Ok((base_transfer, quote_transfer, pnl_transfer, limit_transfer))
Ok((
base_transfer,
quote_transfer_liqee,
quote_transfer_liqor,
platform_fee,
pnl_transfer,
limit_transfer_recurring,
limit_transfer_oneshot,
))
}
#[cfg(test)]
@ -997,7 +1055,7 @@ mod tests {
init_liqee_base,
I80F48::from_num(init_liqee_quote),
);
p.realized_other_pnl_native = p
p.oneshot_settle_pnl_allowance = p
.unsettled_pnl(setup.perp_market.data(), I80F48::ONE)
.unwrap();
@ -1042,7 +1100,8 @@ mod tests {
// The settle limit taken over matches the quote pos when removing the
// quote gains from giving away base lots
assert_eq_f!(
I80F48::from_num(liqor_perp.settle_pnl_limit_realized_trade),
I80F48::from_num(liqor_perp.recurring_settle_pnl_allowance)
+ liqor_perp.oneshot_settle_pnl_allowance,
liqor_perp.quote_position_native.to_num::<f64>()
+ liqor_perp.base_position_lots as f64,
1.1

View File

@ -40,7 +40,13 @@ pub fn perp_liq_force_cancel_orders(
asks: ctx.accounts.asks.load_mut()?,
};
book.cancel_all_orders(&mut account.borrow_mut(), &mut perp_market, limit, None)?;
book.cancel_all_orders(
&mut account.borrow_mut(),
ctx.accounts.account.as_ref().key,
&mut perp_market,
limit,
None,
)?;
let perp_position = account.perp_position(perp_market.perp_market_index)?;
health_cache.recompute_perp_info(perp_position, &perp_market)?;

View File

@ -33,23 +33,24 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
let perp_market = ctx.accounts.perp_market.load()?;
perp_market_index = perp_market.perp_market_index;
settle_token_index = perp_market.settle_token_index;
perp_oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.oracle)?,
Some(now_slot),
)?;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
perp_oracle_price = perp_market
.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
let settle_bank = ctx.accounts.settle_bank.load()?;
let settle_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?;
settle_token_oracle_price = settle_bank.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.settle_oracle)?,
&OracleAccountInfos::from_reader(settle_oracle_ref),
Some(now_slot),
)?;
drop(settle_bank); // could be the same as insurance_bank
let insurance_bank = ctx.accounts.insurance_bank.load()?;
let insurance_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.insurance_oracle.as_ref())?;
// We're not getting the insurance token price from the HealthCache because
// the liqee isn't guaranteed to have an insurance fund token position.
insurance_token_oracle_price = insurance_bank.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.insurance_oracle)?,
&OracleAccountInfos::from_reader(insurance_oracle_ref),
Some(now_slot),
)?;
}
@ -266,7 +267,7 @@ pub(crate) fn liquidation_action(
.max(I80F48::ZERO);
if settlement > 0 {
liqor_perp_position.record_liquidation_quote_change(-settlement);
liqee_perp_position.record_settle(-settlement);
liqee_perp_position.record_settle(-settlement, &perp_market);
// Update the accounts' perp_spot_transfer statistics.
let settlement_i64 = settlement.round_to_zero().to_num::<i64>();
@ -379,7 +380,7 @@ pub(crate) fn liquidation_action(
// transfer perp quote loss from the liqee to the liqor
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
liqee_perp_position.record_settle(-insurance_liab_transfer);
liqee_perp_position.record_settle(-insurance_liab_transfer, &perp_market);
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
msg!(
@ -398,7 +399,7 @@ pub(crate) fn liquidation_action(
(perp_market.long_funding, perp_market.short_funding);
if insurance_fund_exhausted && remaining_liab > 0 {
perp_market.socialize_loss(-remaining_liab)?;
liqee_perp_position.record_settle(-remaining_liab);
liqee_perp_position.record_settle(-remaining_liab, &perp_market);
socialized_loss = remaining_liab;
msg!("socialized loss: {}", socialized_loss);
}
@ -519,25 +520,33 @@ mod tests {
liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
}
let insurance_oracle_ai = setup.insurance_oracle.as_account_info();
let settle_oracle_ai = setup.settle_oracle.as_account_info();
let perp_oracle_ai = setup.perp_oracle.as_account_info();
let insurance_price = setup
.insurance_bank
.data()
.oracle_price(&AccountInfoRef::borrow(&insurance_oracle_ai).unwrap(), None)
.unwrap();
let settle_price = setup
.settle_bank
.data()
.oracle_price(&AccountInfoRef::borrow(&settle_oracle_ai).unwrap(), None)
.unwrap();
let perp_price = setup
.perp_market
.data()
.oracle_price(&AccountInfoRef::borrow(&perp_oracle_ai).unwrap(), None)
.unwrap();
let insurance_price = {
let insurance_oracle_ai = setup.insurance_oracle.as_account_info();
let insurance_oracle_ref = &AccountInfoRef::borrow(&insurance_oracle_ai)?;
setup
.insurance_bank
.data()
.oracle_price(&OracleAccountInfos::from_reader(insurance_oracle_ref), None)
.unwrap()
};
let settle_price = {
let settle_oracle_ai = setup.settle_oracle.as_account_info();
let settle_oracle_ref = &AccountInfoRef::borrow(&settle_oracle_ai)?;
setup
.settle_bank
.data()
.oracle_price(&OracleAccountInfos::from_reader(settle_oracle_ref), None)
.unwrap()
};
let perp_price = {
let perp_oracle_ai = setup.perp_oracle.as_account_info();
let perp_oracle_ref = &AccountInfoRef::borrow(&perp_oracle_ai)?;
setup
.perp_market
.data()
.oracle_price(&OracleAccountInfos::from_reader(perp_oracle_ref), None)
.unwrap()
};
// There's no way to construct a TokenAccount directly...
let mut buffer = [0u8; 165];
@ -751,7 +760,7 @@ mod tests {
{
let p = perp_p(&mut setup.liqee);
p.quote_position_native = I80F48::from_num(init_perp);
p.settle_pnl_limit_realized_trade = -settle_limit;
p.recurring_settle_pnl_allowance = (settle_limit as i64).abs();
let settle_bank = setup.settle_bank.data();
settle_bank

View File

@ -30,8 +30,9 @@ pub fn perp_place_order(
asks: ctx.accounts.asks.load_mut()?,
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_state = perp_market.oracle_state(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
None, // staleness checked in health
)?;
oracle_price = oracle_state.price;

View File

@ -29,12 +29,14 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
);
// Get oracle prices
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
None, // staleness checked in health
)?;
let settle_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?;
let settle_token_oracle_price = settle_bank.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?,
&OracleAccountInfos::from_reader(settle_oracle_ref),
None, // staleness checked in health
)?;
@ -66,7 +68,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
.min(I80F48::from(max_settle_amount));
require!(settlement >= 0, MangoError::SettlementAmountMustBePositive);
perp_position.record_settle(-settlement); // settle the negative pnl on the user perp position
perp_position.record_settle(-settlement, &perp_market); // settle the negative pnl on the user perp position
perp_market.fees_accrued -= settlement;
emit_perp_balances(

View File

@ -62,12 +62,14 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
);
// Get oracle prices
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
None, // staleness checked in health
)?;
let settle_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?;
let settle_token_oracle_price = settle_bank.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?,
&OracleAccountInfos::from_reader(settle_oracle_ref),
None, // staleness checked in health
)?;
@ -141,8 +143,8 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
b_max_settle,
);
a_perp_position.record_settle(settlement);
b_perp_position.record_settle(-settlement);
a_perp_position.record_settle(settlement, &perp_market);
b_perp_position.record_settle(-settlement, &perp_market);
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.account_a.key(),

View File

@ -14,10 +14,9 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
};
let now_slot = Clock::get()?.slot;
let oracle_state = perp_market.oracle_state(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
)?;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_state =
perp_market.oracle_state(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
perp_market.update_funding_and_stable_price(&book, &oracle_state, now_ts)?;

View File

@ -0,0 +1,78 @@
use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
pub fn serum3_cancel_order_by_client_order_id(
ctx: Context<Serum3CancelOrder>,
client_order_id: u64,
) -> Result<()> {
let serum_market = ctx.accounts.serum_market.load()?;
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
);
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
}
//
// Cancel
//
cpi_cancel_order_by_client_order_id(ctx.accounts, client_order_id)?;
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit_stack(Serum3OpenOrdersBalanceLogV2 {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
market_index: serum_market.market_index,
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}
fn cpi_cancel_order_by_client_order_id(
ctx: &Serum3CancelOrder,
client_order_id: u64,
) -> Result<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;
serum3_cpi::CancelOrder {
program: ctx.serum_program.to_account_info(),
market: ctx.serum_market_external.to_account_info(),
bids: ctx.market_bids.to_account_info(),
asks: ctx.market_asks.to_account_info(),
event_queue: ctx.market_event_queue.to_account_info(),
open_orders: ctx.open_orders.to_account_info(),
open_orders_authority: ctx.group.to_account_info(),
}
.cancel_one_by_client_order_id(&group, client_order_id)
}

View File

@ -316,23 +316,19 @@ pub fn serum3_place_order(
)?
};
// Deposit limit check: Placing an order can increase deposit limit use on both
// the payer and receiver bank. Imagine placing a bid for 500 base @ 0.5: it would
// use up 1000 quote and 500 base because either could be deposit on cancel/fill.
// This is why this must happen after update_bank_potential_tokens() and any withdraws.
// Deposit limit check, receiver side:
// Placing an order can always increase the receiver bank deposits on fill.
{
let receiver_bank = receiver_bank_ai.load::<Bank>()?;
receiver_bank
.check_deposit_and_oo_limit()
.with_context(|| std::format!("on {}", receiver_bank.name()))?;
payer_bank
.check_deposit_and_oo_limit()
.with_context(|| std::format!("on {}", payer_bank.name()))?;
}
// Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio
let payer_oracle_ref = &AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?;
let payer_bank_oracle =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
payer_bank.oracle_price(&OracleAccountInfos::from_reader(payer_oracle_ref), None)?;
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
if withdrawn_from_vault > before_position_native {
require_msg_typed!(
@ -342,6 +338,18 @@ pub fn serum3_place_order(
);
payer_bank.enforce_max_utilization_on_borrow()?;
payer_bank.check_net_borrows(payer_bank_oracle)?;
// Deposit limit check, payer side:
// The payer bank deposits could increase when cancelling the order later:
// Imagine the account borrowing payer tokens to place the order, repaying the borrows
// and then cancelling the order to create a deposit.
//
// However, if the account only decreases its deposits to place an order it can't
// worsen the situation and should always go through, even if payer deposit limits are
// already exceeded.
payer_bank
.check_deposit_and_oo_limit()
.with_context(|| std::format!("on {}", payer_bank.name()))?;
} else {
payer_bank.enforce_borrows_lte_deposits()?;
}
@ -570,8 +578,11 @@ pub fn apply_settle_changes(
let clock = Clock::get()?;
let now_ts = clock.unix_timestamp.try_into().unwrap();
let quote_oracle_price = quote_bank
.oracle_price(&AccountInfoRef::borrow(quote_oracle_ai)?, Some(clock.slot))?;
let quote_oracle_ref = &AccountInfoRef::borrow(quote_oracle_ai)?;
let quote_oracle_price = quote_bank.oracle_price(
&OracleAccountInfos::from_reader(quote_oracle_ref),
Some(clock.slot),
)?;
let quote_asset_price = quote_oracle_price.min(quote_bank.stable_price());
account
.fixed

View File

@ -185,7 +185,11 @@ pub fn charge_loan_origination_fees(
let base_oracle_price = base_oracle
.map(|ai| {
base_bank.oracle_price(&AccountInfoRef::borrow(ai)?, Some(Clock::get()?.slot))
let ai_ref = &AccountInfoRef::borrow(ai)?;
base_bank.oracle_price(
&OracleAccountInfos::from_reader(ai_ref),
Some(Clock::get()?.slot),
)
})
.transpose()?;
@ -221,7 +225,11 @@ pub fn charge_loan_origination_fees(
let quote_oracle_price = quote_oracle
.map(|ai| {
quote_bank.oracle_price(&AccountInfoRef::borrow(ai)?, Some(Clock::get()?.slot))
let ai_ref = &AccountInfoRef::borrow(ai)?;
quote_bank.oracle_price(
&OracleAccountInfos::from_reader(ai_ref),
Some(Clock::get()?.slot),
)
})
.transpose()?;

View File

@ -90,8 +90,9 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
// Get the oracle price, even if stale or unconfident: We want to allow users
// to deposit to close borrows or do other fixes even if the oracle is bad.
let oracle_ref = &AccountInfoRef::borrow(self.oracle.as_ref())?;
let unsafe_oracle_state = oracle_state_unchecked(
&AccountInfoRef::borrow(self.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
bank.mint_decimals,
)?;
let unsafe_oracle_price = unsafe_oracle_state.price;
@ -129,16 +130,14 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
// 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.
// Being in a health region always means being_liquidated is false, so it's safe to gate the check.
if !account.fixed.is_in_health_region() {
let was_being_liquidated = account.being_liquidated();
if !account.fixed.is_in_health_region() && was_being_liquidated {
let health = cache.health(HealthType::LiquidationEnd);
msg!("health: {}", health);
// Only compute health and check for recovery if not already being liquidated
let was_being_liquidated = account.being_liquidated();
let recovered = account.fixed.maybe_recover_from_being_liquidated(health);
require!(
!was_being_liquidated || recovered,
MangoError::DepositsIntoLiquidatingMustRecover
);
require!(recovered, MangoError::DepositsIntoLiquidatingMustRecover);
}
// Group level deposit limit on account
@ -168,19 +167,6 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
account.deactivate_token_position_and_log(raw_token_index, self.account.key());
}
unsafe {
const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize;
msg!("heap {}", *POS_PTR);
}
// emit_stack(DepositLog {
// mango_group: self.group.key(),
// mango_account: self.account.key(),
// signer: self.token_authority.key(),
// token_index,
// quantity: amount_i80f48.to_num::<u64>(),
// price: unsafe_oracle_price.to_bits(),
// });
emit_stack(DepositLog {
mango_group: self.group.key(),
mango_account: self.account.key(),
@ -190,11 +176,6 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
price: unsafe_oracle_price.to_bits(),
});
unsafe {
const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize;
msg!("heap {}", *POS_PTR);
}
Ok(())
}
}
@ -214,10 +195,9 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64, reduce_only: bool)
let now_slot = Clock::get()?.slot;
let bank = ctx.accounts.bank.load()?;
let oracle_result = bank.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
);
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_result =
bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot));
if let Err(e) = oracle_result {
msg!("oracle must be valid when creating a new token position");
return Err(e);

View File

@ -8,7 +8,7 @@ use crate::error::MangoError;
use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_stack, TokenMetaDataLog};
use crate::logs::{emit_stack, TokenMetaDataLogV2};
use crate::util::fill_from_str;
#[allow(unused_variables)]
@ -49,8 +49,10 @@ pub fn token_edit(
maint_weight_shift_asset_target_opt: Option<f32>,
maint_weight_shift_liab_target_opt: Option<f32>,
maint_weight_shift_abort: bool,
set_fallback_oracle: bool, // unused, introduced in v0.22
set_fallback_oracle: bool,
deposit_limit_opt: Option<u64>,
zero_util_rate: Option<f32>,
platform_liquidation_fee: Option<f32>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -78,11 +80,25 @@ pub fn token_edit(
mint_info.oracle = oracle;
require_group_admin = true;
}
if set_fallback_oracle {
msg!(
"Fallback oracle old {:?}, new {:?}",
bank.fallback_oracle,
ctx.accounts.fallback_oracle.key()
);
check_is_valid_fallback_oracle(&AccountInfoRef::borrow(
ctx.accounts.fallback_oracle.as_ref(),
)?)?;
bank.fallback_oracle = ctx.accounts.fallback_oracle.key();
mint_info.fallback_oracle = ctx.accounts.fallback_oracle.key();
require_group_admin = true;
}
if reset_stable_price {
msg!("Stable price reset");
require_keys_eq!(bank.oracle, ctx.accounts.oracle.key());
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let oracle_price =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)?;
bank.stable_price_model.reset_to_price(
oracle_price.to_num(),
Clock::get()?.unix_timestamp.try_into().unwrap(),
@ -448,6 +464,26 @@ pub fn token_edit(
bank.deposit_limit = deposit_limit;
require_group_admin = true;
}
if let Some(zero_util_rate) = zero_util_rate {
msg!(
"Zero utilization rate old {:?}, new {:?}",
bank.zero_util_rate,
zero_util_rate
);
bank.zero_util_rate = I80F48::from_num(zero_util_rate);
require_group_admin = true;
}
if let Some(platform_liquidation_fee) = platform_liquidation_fee {
msg!(
"Platform liquidation fee old {:?}, new {:?}",
bank.platform_liquidation_fee,
platform_liquidation_fee
);
bank.platform_liquidation_fee = I80F48::from_num(platform_liquidation_fee);
require_group_admin = true;
}
}
// account constraint #1
@ -468,12 +504,13 @@ pub fn token_edit(
let bank = ctx.remaining_accounts.first().unwrap().load_mut::<Bank>()?;
bank.verify()?;
emit_stack(TokenMetaDataLog {
emit_stack(TokenMetaDataLogV2 {
mango_group: ctx.accounts.group.key(),
mint: mint_info.mint.key(),
token_index: bank.token_index,
mint_decimals: bank.mint_decimals,
oracle: mint_info.oracle.key(),
fallback_oracle: ctx.accounts.fallback_oracle.key(),
mint_info: ctx.accounts.mint_info.key(),
});

View File

@ -1,7 +1,7 @@
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::logs::{emit_stack, TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog};
use crate::logs::{emit_stack, TokenBalanceLog, TokenForceCloseBorrowsWithTokenLogV2};
use crate::state::*;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
@ -62,12 +62,18 @@ pub fn token_force_close_borrows_with_token(
MangoError::TokenInReduceOnlyMode
);
let fee_factor_liqor =
(I80F48::ONE + liab_bank.liquidation_fee) * (I80F48::ONE + asset_bank.liquidation_fee);
let fee_factor_total =
(I80F48::ONE + liab_bank.liquidation_fee + liab_bank.platform_liquidation_fee)
* (I80F48::ONE + asset_bank.liquidation_fee + asset_bank.platform_liquidation_fee);
// account constraint #3
// only allow combination of asset and liab token,
// where liqee's health would be guaranteed to not decrease
require_gte!(
liab_bank.init_liab_weight,
asset_bank.init_liab_weight * (I80F48::ONE + liab_bank.liquidation_fee),
asset_bank.init_liab_weight * fee_factor_total,
MangoError::SomeError
);
@ -95,9 +101,13 @@ pub fn token_force_close_borrows_with_token(
.max(I80F48::ZERO);
// The amount of asset native tokens we will give up for them
let fee_factor = I80F48::ONE + liab_bank.liquidation_fee;
let liab_oracle_price_adjusted = liab_oracle_price * fee_factor;
let asset_transfer = liab_transfer * liab_oracle_price_adjusted / asset_oracle_price;
let asset_transfer_base = liab_transfer * liab_oracle_price / asset_oracle_price;
let asset_transfer_to_liqor = asset_transfer_base * fee_factor_liqor;
let asset_transfer_from_liqee = asset_transfer_base * fee_factor_total;
let asset_liquidation_fee = asset_transfer_from_liqee - asset_transfer_to_liqor;
asset_bank.collected_fees_native += asset_liquidation_fee;
asset_bank.collected_liquidation_fees += asset_liquidation_fee;
// Apply the balance changes to the liqor and liqee accounts
let liqee_liab_active =
@ -112,13 +122,13 @@ pub fn token_force_close_borrows_with_token(
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
let liqor_asset_active =
asset_bank.deposit(liqor_asset_position, asset_transfer, now_ts)?;
asset_bank.deposit(liqor_asset_position, asset_transfer_to_liqor, now_ts)?;
let liqor_asset_indexed_position = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index);
let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(
liqee_asset_position,
asset_transfer,
asset_transfer_from_liqee,
now_ts,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
@ -127,7 +137,7 @@ pub fn token_force_close_borrows_with_token(
msg!(
"Force closed {} liab for {} asset",
liab_transfer,
asset_transfer
asset_transfer_from_liqee,
);
// liqee asset
@ -167,17 +177,19 @@ pub fn token_force_close_borrows_with_token(
borrow_index: liab_bank.borrow_index.to_bits(),
});
emit_stack(TokenForceCloseBorrowsWithTokenLog {
emit_stack(TokenForceCloseBorrowsWithTokenLogV2 {
mango_group: liqee.fixed.group,
liqee: liqee_key,
liqor: liqor_key,
asset_token_index: asset_token_index,
liab_token_index: liab_token_index,
asset_transfer: asset_transfer.to_bits(),
asset_transfer_from_liqee: asset_transfer_from_liqee.to_bits(),
asset_transfer_to_liqor: asset_transfer_to_liqor.to_bits(),
asset_liquidation_fee: asset_liquidation_fee.to_bits(),
liab_transfer: liab_transfer.to_bits(),
asset_price: asset_oracle_price.to_bits(),
liab_price: liab_oracle_price.to_bits(),
fee_factor: fee_factor.to_bits(),
fee_factor: fee_factor_total.to_bits(),
});
// liqor should never have a borrow

View File

@ -5,7 +5,7 @@ use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::logs::{
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog,
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLogV2,
WithdrawLoanLog,
};
use crate::state::*;
@ -118,17 +118,26 @@ pub(crate) fn liquidation_action(
let liqee_liab_native = liqee_liab_position.native(liab_bank);
require_gt!(0, liqee_liab_native);
// Liquidation fees work by giving the liqor more assets than the oracle price would
// indicate. Specifically we choose
// The liqor will likely close the borrow by buying liab tokens somewhere and get rid of the
// asset tokens by selling them. Both transactions may incur slippage, so to make sure liqors
// are willing to perform the liquidation, they receive a liquidation fee.
//
// Liquidation fees work by giving the liqor more assets than the oracle price would indicate.
//
// Specifically we choose
// assets =
// liabs * liab_oracle_price/asset_oracle_price * (1 + liab_liq_fee)
// Which means that we use a increased liab oracle price for the conversion.
// liabs * liab_oracle_price/asset_oracle_price * (1 + liab_liq_fee) * (1 + asset_liq_fee)
// Which is equivalent to using an increased liab oracle price for the conversion.
// For simplicity we write
// assets = liabs * liab_oracle_price / asset_oracle_price * fee_factor
// assets = liabs * liab_oracle_price_adjusted / asset_oracle_price
// = liabs * lopa / aop
let fee_factor = I80F48::ONE + liab_bank.liquidation_fee;
let liab_oracle_price_adjusted = liab_oracle_price * fee_factor;
let fee_factor_liqor =
(I80F48::ONE + liab_bank.liquidation_fee) * (I80F48::ONE + asset_bank.liquidation_fee);
let fee_factor_total =
(I80F48::ONE + liab_bank.liquidation_fee + liab_bank.platform_liquidation_fee)
* (I80F48::ONE + asset_bank.liquidation_fee + asset_bank.platform_liquidation_fee);
let liab_oracle_price_adjusted = liab_oracle_price * fee_factor_total;
let init_asset_weight = asset_bank.init_asset_weight;
let init_liab_weight = liab_bank.init_liab_weight;
@ -212,7 +221,13 @@ pub(crate) fn liquidation_action(
.max(I80F48::ZERO);
// The amount of asset native tokens we will give up for them
let asset_transfer = liab_transfer * liab_oracle_price_adjusted / asset_oracle_price;
let asset_transfer_base = liab_transfer * liab_oracle_price / asset_oracle_price;
let asset_transfer_to_liqor = asset_transfer_base * fee_factor_liqor;
let asset_transfer_from_liqee = asset_transfer_base * fee_factor_total;
let asset_liquidation_fee = asset_transfer_from_liqee - asset_transfer_to_liqor;
asset_bank.collected_fees_native += asset_liquidation_fee;
asset_bank.collected_liquidation_fees += asset_liquidation_fee;
// During liquidation, we mustn't leave small positive balances in the liqee. Those
// could break bankruptcy-detection. Thus we dust them even if the token position
@ -233,13 +248,14 @@ pub(crate) fn liquidation_action(
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer, now_ts)?;
let liqor_asset_active =
asset_bank.deposit(liqor_asset_position, asset_transfer_to_liqor, now_ts)?;
let liqor_asset_indexed_position = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index);
let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(
liqee_asset_position,
asset_transfer,
asset_transfer_from_liqee,
now_ts,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
@ -254,7 +270,7 @@ pub(crate) fn liquidation_action(
msg!(
"liquidated {} liab for {} asset",
liab_transfer,
asset_transfer
asset_transfer_from_liqee,
);
// liqee asset
@ -329,13 +345,15 @@ pub(crate) fn liquidation_action(
.fixed
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
emit_stack(TokenLiqWithTokenLog {
emit_stack(TokenLiqWithTokenLogV2 {
mango_group: liqee.fixed.group,
liqee: liqee_key,
liqor: liqor_key,
asset_token_index,
liab_token_index,
asset_transfer: asset_transfer.to_bits(),
asset_transfer_from_liqee: asset_transfer_from_liqee.to_bits(),
asset_transfer_to_liqor: asset_transfer_to_liqor.to_bits(),
asset_liquidation_fee: asset_liquidation_fee.to_bits(),
liab_transfer: liab_transfer.to_bits(),
asset_price: asset_oracle_price.to_bits(),
liab_price: liab_oracle_price.to_bits(),

View File

@ -6,7 +6,7 @@ use crate::error::*;
use crate::state::*;
use crate::util::fill_from_str;
use crate::logs::{emit_stack, TokenMetaDataLog};
use crate::logs::{emit_stack, TokenMetaDataLogV2};
pub const INDEX_START: I80F48 = I80F48::from_bits(1_000_000 * I80F48::ONE.to_bits());
@ -42,6 +42,8 @@ pub fn token_register(
interest_target_utilization: f32,
group_insurance_fund: bool,
deposit_limit: u64,
zero_util_rate: f32,
platform_liquidation_fee: f32,
) -> Result<()> {
// Require token 0 to be in the insurance token
if token_index == INSURANCE_TOKEN_INDEX {
@ -120,13 +122,16 @@ pub fn token_register(
maint_weight_shift_duration_inv: I80F48::ZERO,
maint_weight_shift_asset_target: I80F48::ZERO,
maint_weight_shift_liab_target: I80F48::ZERO,
fallback_oracle: Pubkey::default(), // unused, introduced in v0.22
fallback_oracle: ctx.accounts.fallback_oracle.key(),
deposit_limit,
reserved: [0; 1968],
zero_util_rate: I80F48::from_num(zero_util_rate),
platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee),
collected_liquidation_fees: I80F48::ZERO,
reserved: [0; 1920],
};
if let Ok(oracle_price) =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)
{
bank.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
@ -135,6 +140,9 @@ pub fn token_register(
}
bank.verify()?;
check_is_valid_fallback_oracle(&AccountInfoRef::borrow(
ctx.accounts.fallback_oracle.as_ref(),
)?)?;
let mut mint_info = ctx.accounts.mint_info.load_init()?;
*mint_info = MintInfo {
@ -146,19 +154,21 @@ pub fn token_register(
banks: Default::default(),
vaults: Default::default(),
oracle: ctx.accounts.oracle.key(),
fallback_oracle: ctx.accounts.fallback_oracle.key(),
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
reserved: [0; 2560],
reserved: [0; 2528],
};
mint_info.banks[0] = ctx.accounts.bank.key();
mint_info.vaults[0] = ctx.accounts.vault.key();
emit_stack(TokenMetaDataLog {
emit_stack(TokenMetaDataLogV2 {
mango_group: ctx.accounts.group.key(),
mint: ctx.accounts.mint.key(),
token_index,
mint_decimals: ctx.accounts.mint.decimals,
oracle: ctx.accounts.oracle.key(),
fallback_oracle: ctx.accounts.fallback_oracle.key(),
mint_info: ctx.accounts.mint_info.key(),
});

View File

@ -7,7 +7,7 @@ use crate::instructions::INDEX_START;
use crate::state::*;
use crate::util::fill_from_str;
use crate::logs::{emit_stack, TokenMetaDataLog};
use crate::logs::{emit_stack, TokenMetaDataLogV2};
use crate::accounts_ix::*;
@ -71,7 +71,8 @@ pub fn token_register_trustless(
init_asset_weight: I80F48::from_num(0),
maint_liab_weight: I80F48::from_num(1.4), // 2.5x
init_liab_weight: I80F48::from_num(1.8), // 1.25x
liquidation_fee: I80F48::from_num(0.2),
liquidation_fee: I80F48::from_num(0.05),
platform_liquidation_fee: I80F48::from_num(0.05),
dust: I80F48::ZERO,
flash_loan_token_account_initial: u64::MAX,
flash_loan_approved_amount: 0,
@ -102,13 +103,14 @@ pub fn token_register_trustless(
maint_weight_shift_duration_inv: I80F48::ZERO,
maint_weight_shift_asset_target: I80F48::ZERO,
maint_weight_shift_liab_target: I80F48::ZERO,
fallback_oracle: Pubkey::default(), // unused, introduced in v0.22
fallback_oracle: ctx.accounts.fallback_oracle.key(),
deposit_limit: 0,
reserved: [0; 1968],
zero_util_rate: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO,
reserved: [0; 1920],
};
if let Ok(oracle_price) =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)
{
bank.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
@ -117,6 +119,9 @@ pub fn token_register_trustless(
}
bank.verify()?;
check_is_valid_fallback_oracle(&AccountInfoRef::borrow(
ctx.accounts.fallback_oracle.as_ref(),
)?)?;
let mut mint_info = ctx.accounts.mint_info.load_init()?;
*mint_info = MintInfo {
@ -128,19 +133,21 @@ pub fn token_register_trustless(
banks: Default::default(),
vaults: Default::default(),
oracle: ctx.accounts.oracle.key(),
fallback_oracle: ctx.accounts.fallback_oracle.key(),
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
reserved: [0; 2560],
reserved: [0; 2528],
};
mint_info.banks[0] = ctx.accounts.bank.key();
mint_info.vaults[0] = ctx.accounts.vault.key();
emit_stack(TokenMetaDataLog {
emit_stack(TokenMetaDataLogV2 {
mango_group: ctx.accounts.group.key(),
mint: ctx.accounts.mint.key(),
token_index,
mint_decimals: ctx.accounts.mint.decimals,
oracle: ctx.accounts.oracle.key(),
fallback_oracle: ctx.accounts.fallback_oracle.key(),
mint_info: ctx.accounts.mint_info.key(),
});

View File

@ -3,7 +3,7 @@ use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::error::MangoError;
use crate::logs::{emit_stack, UpdateIndexLog, UpdateRateLogV2};
use crate::state::HOUR;
use crate::state::{OracleAccountInfos, HOUR};
use crate::{
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
state::Bank,
@ -89,8 +89,9 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
now_ts,
);
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let price = some_bank.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
Some(clock.slot),
)?;

View File

@ -3,6 +3,7 @@ use crate::error::*;
use crate::health::*;
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::associated_token;
use anchor_spl::token;
use fixed::types::I80F48;
@ -11,6 +12,8 @@ use crate::logs::{
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanLog, WithdrawLog,
};
const DELEGATE_WITHDRAW_MAX: i64 = 100_000; // $0.1
pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
require_msg!(amount > 0, "withdraw amount must be positive");
@ -70,8 +73,9 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// Get the oracle price, even if stale or unconfident: We want to allow users
// to withdraw deposits (while staying healthy otherwise) if the oracle is bad.
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let unsafe_oracle_state = oracle_state_unchecked(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
&OracleAccountInfos::from_reader(oracle_ref),
bank.mint_decimals,
)?;
@ -81,6 +85,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
amount_i80f48,
Clock::get()?.unix_timestamp.try_into().unwrap(),
)?;
let native_position_after = position.native(&bank);
// Avoid getting in trouble because of the mutable bank account borrow later
drop(bank);
@ -103,8 +108,6 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
amount,
)?;
let native_position_after = position.native(&bank);
emit_stack(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
@ -118,6 +121,38 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let amount_usd = (amount_i80f48 * unsafe_oracle_state.price).to_num::<i64>();
account.fixed.net_deposits -= amount_usd;
// Delegates have heavy restrictions on withdraws. #1
if account.fixed.is_delegate(ctx.accounts.owner.key()) {
// Delegates can only withdrawing into the actual owner's ATA
let owner_ata = associated_token::get_associated_token_address(
&account.fixed.owner,
&ctx.accounts.vault.mint,
);
require_keys_eq!(
ctx.accounts.token_account.key(),
owner_ata,
MangoError::DelegateWithdrawOnlyToOwnerAta
);
require_keys_eq!(
ctx.accounts.token_account.owner,
account.fixed.owner,
MangoError::DelegateWithdrawOnlyToOwnerAta
);
// Delegates must close the token position
require!(
!withdraw_result.position_is_active,
MangoError::DelegateWithdrawMustClosePosition
);
// Delegates can't withdraw too much
require_gte!(
DELEGATE_WITHDRAW_MAX,
amount_usd,
MangoError::DelegateWithdrawSmall
);
}
//
// Health check
//
@ -180,11 +215,17 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// When borrowing the price has be trustworthy, so we can do a reasonable
// net borrow check.
unsafe_oracle_state.check_confidence_and_maybe_staleness(
&bank.oracle,
&bank.oracle_config,
Some(Clock::get()?.slot),
)?;
let slot_opt = Some(Clock::get()?.slot);
unsafe_oracle_state
.check_confidence_and_maybe_staleness(&bank.oracle_config, slot_opt)
.with_context(|| {
oracle_log_context(
bank.name(),
&unsafe_oracle_state,
&bank.oracle_config,
slot_opt,
)
})?;
bank.check_net_borrows(unsafe_oracle_state.price)?;
} else {
bank.enforce_borrows_lte_deposits()?;

View File

@ -15,6 +15,7 @@ use accounts_ix::*;
pub mod accounts_ix;
pub mod accounts_zerocopy;
pub mod address_lookup_table_program;
mod allocator;
pub mod error;
pub mod events;
pub mod health;
@ -154,6 +155,8 @@ pub mod mango_v4 {
interest_target_utilization: f32,
group_insurance_fund: bool,
deposit_limit: u64,
zero_util_rate: f32,
platform_liquidation_fee: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_register(
@ -185,6 +188,8 @@ pub mod mango_v4 {
interest_target_utilization,
group_insurance_fund,
deposit_limit,
zero_util_rate,
platform_liquidation_fee,
)?;
Ok(())
}
@ -236,8 +241,10 @@ pub mod mango_v4 {
maint_weight_shift_asset_target_opt: Option<f32>,
maint_weight_shift_liab_target_opt: Option<f32>,
maint_weight_shift_abort: bool,
set_fallback_oracle: bool, // unused, introduced in v0.22
set_fallback_oracle: bool,
deposit_limit_opt: Option<u64>,
zero_util_rate_opt: Option<f32>,
platform_liquidation_fee_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_edit(
@ -278,6 +285,8 @@ pub mod mango_v4 {
maint_weight_shift_abort,
set_fallback_oracle,
deposit_limit_opt,
zero_util_rate_opt,
platform_liquidation_fee_opt,
)?;
Ok(())
}
@ -681,6 +690,15 @@ pub mod mango_v4 {
Ok(())
}
pub fn serum3_cancel_order_by_client_order_id(
ctx: Context<Serum3CancelOrder>,
client_order_id: u64,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_cancel_order_by_client_order_id(ctx, client_order_id)?;
Ok(())
}
pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_cancel_all_orders(ctx, limit)?;
@ -823,6 +841,7 @@ pub mod mango_v4 {
settle_pnl_limit_factor: f32,
settle_pnl_limit_window_size_ts: u64,
positive_pnl_liquidation_fee: f32,
platform_liquidation_fee: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_create_market(
@ -854,6 +873,7 @@ pub mod mango_v4 {
settle_pnl_limit_factor,
settle_pnl_limit_window_size_ts,
positive_pnl_liquidation_fee,
platform_liquidation_fee,
)?;
Ok(())
}
@ -891,6 +911,7 @@ pub mod mango_v4 {
positive_pnl_liquidation_fee_opt: Option<f32>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
platform_liquidation_fee_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_edit_market(
@ -925,6 +946,7 @@ pub mod mango_v4 {
positive_pnl_liquidation_fee_opt,
name_opt,
force_close_opt,
platform_liquidation_fee_opt,
)?;
Ok(())
}

View File

@ -344,6 +344,22 @@ pub struct TokenLiqWithTokenLog {
pub bankruptcy: bool,
}
#[event]
pub struct TokenLiqWithTokenLogV2 {
pub mango_group: Pubkey,
pub liqee: Pubkey,
pub liqor: Pubkey,
pub asset_token_index: u16,
pub liab_token_index: u16,
pub asset_transfer_from_liqee: i128, // I80F48
pub asset_transfer_to_liqor: i128, // I80F48
pub asset_liquidation_fee: i128, // I80F48
pub liab_transfer: i128, // I80F48
pub asset_price: i128, // I80F48
pub liab_price: i128, // I80F48
pub bankruptcy: bool,
}
#[event]
pub struct Serum3OpenOrdersBalanceLog {
pub mango_group: Pubkey,
@ -450,6 +466,17 @@ pub struct TokenMetaDataLog {
pub mint_info: Pubkey,
}
#[event]
pub struct TokenMetaDataLogV2 {
pub mango_group: Pubkey,
pub mint: Pubkey,
pub token_index: u16,
pub mint_decimals: u8,
pub oracle: Pubkey,
pub fallback_oracle: Pubkey,
pub mint_info: Pubkey,
}
#[event]
pub struct PerpMarketMetaDataLog {
pub mango_group: Pubkey,
@ -485,6 +512,37 @@ pub struct PerpLiqBaseOrPositivePnlLog {
pub price: i128,
}
#[event]
pub struct PerpLiqBaseOrPositivePnlLogV2 {
pub mango_group: Pubkey,
pub perp_market_index: u16,
pub liqor: Pubkey,
pub liqee: Pubkey,
pub base_transfer_liqee: i64,
pub quote_transfer_liqee: i128,
pub quote_transfer_liqor: i128,
pub quote_platform_fee: i128,
pub pnl_transfer: i128,
pub pnl_settle_limit_transfer: i128,
pub price: i128,
}
#[event]
pub struct PerpLiqBaseOrPositivePnlLogV3 {
pub mango_group: Pubkey,
pub perp_market_index: u16,
pub liqor: Pubkey,
pub liqee: Pubkey,
pub base_transfer_liqee: i64,
pub quote_transfer_liqee: i128,
pub quote_transfer_liqor: i128,
pub quote_platform_fee: i128,
pub pnl_transfer: i128,
pub pnl_settle_limit_transfer_recurring: i64,
pub pnl_settle_limit_transfer_oneshot: i64,
pub price: i128,
}
#[event]
pub struct PerpLiqBankruptcyLog {
pub mango_group: Pubkey,
@ -583,6 +641,23 @@ pub struct TokenForceCloseBorrowsWithTokenLog {
pub fee_factor: i128,
}
#[event]
pub struct TokenForceCloseBorrowsWithTokenLogV2 {
pub mango_group: Pubkey,
pub liqor: Pubkey,
pub liqee: Pubkey,
pub asset_token_index: u16,
pub liab_token_index: u16,
pub asset_transfer_from_liqee: i128, // I80F48
pub asset_transfer_to_liqor: i128, // I80F48
pub asset_liquidation_fee: i128, // I80F48
pub liab_transfer: i128, // I80F48
pub asset_price: i128, // I80F48
pub liab_price: i128, // I80F48
/// including liqor and platform liquidation fees
pub fee_factor: i128, // I80F48
}
#[event]
pub struct TokenConditionalSwapCreateLog {
pub mango_group: Pubkey,

View File

@ -1,5 +1,5 @@
use anchor_lang::prelude::*;
use serum_dex::state::{OpenOrders, ToAlignedBytes};
use serum_dex::state::{OpenOrders, ToAlignedBytes, ACCOUNT_HEAD_PADDING};
use std::cell::{Ref, RefMut};
use std::cmp::min;
@ -49,6 +49,14 @@ fn strip_data_header_mut<H: bytemuck::Pod, D: bytemuck::Pod>(
}))
}
pub fn has_serum_header(data: &[u8]) -> bool {
if data.len() < 5 {
return false;
}
let head = &data[..5];
head == ACCOUNT_HEAD_PADDING
}
pub fn load_market_state<'a>(
market_account: &'a AccountInfo,
program_id: &Pubkey,
@ -485,6 +493,38 @@ impl<'a> CancelOrder<'a> {
Ok(())
}
pub fn cancel_one_by_client_order_id(self, group: &Group, client_order_id: u64) -> Result<()> {
let data =
serum_dex::instruction::MarketInstruction::CancelOrderByClientIdV2(client_order_id)
.pack();
let instruction = solana_program::instruction::Instruction {
program_id: *self.program.key,
data,
accounts: vec![
AccountMeta::new(*self.market.key, false),
AccountMeta::new(*self.bids.key, false),
AccountMeta::new(*self.asks.key, false),
AccountMeta::new(*self.open_orders.key, false),
AccountMeta::new_readonly(*self.open_orders_authority.key, true),
AccountMeta::new(*self.event_queue.key, false),
],
};
let account_infos = [
self.program,
self.market,
self.bids,
self.asks,
self.open_orders,
self.open_orders_authority,
self.event_queue,
];
let seeds = group_seeds!(group);
solana_program::program::invoke_signed_unchecked(&instruction, &account_infos, &[seeds])?;
Ok(())
}
pub fn cancel_all(self, group: &Group, mut limit: u8) -> Result<()> {
// find all cancels by scanning open_orders/bids/asks
let mut cancels = vec![];

View File

@ -1,4 +1,4 @@
use super::{OracleConfig, TokenIndex, TokenPosition};
use super::{OracleAccountInfos, OracleConfig, TokenIndex, TokenPosition};
use crate::accounts_zerocopy::KeyedAccountReader;
use crate::error::*;
use crate::i80f48::ClampToInt;
@ -8,6 +8,7 @@ use crate::util;
use anchor_lang::prelude::*;
use derivative::Derivative;
use fixed::types::I80F48;
use oracle::oracle_log_context;
use static_assertions::const_assert_eq;
use std::mem::size_of;
@ -58,14 +59,32 @@ pub struct Bank {
pub avg_utilization: I80F48,
pub adjustment_factor: I80F48,
/// The unscaled borrow interest curve is defined as continuous piecewise linear with the points:
///
/// - 0% util: zero_util_rate
/// - util0% util: rate0
/// - util1% util: rate1
/// - 100% util: max_rate
///
/// The final rate is this unscaled curve multiplied by interest_curve_scaling.
pub util0: I80F48,
pub rate0: I80F48,
pub util1: I80F48,
pub rate1: I80F48,
/// the 100% utilization rate
///
/// This isn't the max_rate, since this still gets scaled by interest_curve_scaling,
/// which is >=1.
pub max_rate: I80F48,
// TODO: add ix/logic to regular send this to DAO
/// Fees collected over the lifetime of the bank
///
/// See fees_withdrawn for how much of the fees was withdrawn.
/// See collected_liquidation_fees for the (included) subtotal for liquidation related fees.
pub collected_fees_native: I80F48,
pub loan_origination_fee_rate: I80F48,
pub loan_fee_rate: I80F48,
@ -77,9 +96,13 @@ pub struct Bank {
pub maint_liab_weight: I80F48,
pub init_liab_weight: I80F48,
// a fraction of the price, like 0.05 for a 5% fee during liquidation
//
// Liquidation always involves two tokens, and the sum of the two configured fees is used.
/// Liquidation fee that goes to the liqor.
///
/// Liquidation always involves two tokens, and the sum of the two configured fees is used.
///
/// A fraction of the price, like 0.05 for a 5% fee during liquidation.
///
/// See also platform_liquidation_fee.
pub liquidation_fee: I80F48,
// Collection of all fractions-of-native-tokens that got rounded away
@ -161,18 +184,41 @@ pub struct Bank {
/// serum open order execution.
pub potential_serum_tokens: u64,
/// Start timestamp in seconds at which maint weights should start to change away
/// from maint_asset_weight, maint_liab_weight towards _asset_target and _liab_target.
/// If _start and _end and _duration_inv are 0, no shift is configured.
pub maint_weight_shift_start: u64,
/// End timestamp in seconds until which the maint weights should reach the configured targets.
pub maint_weight_shift_end: u64,
/// Cache of the inverse of maint_weight_shift_end - maint_weight_shift_start,
/// or zero if no shift is configured
pub maint_weight_shift_duration_inv: I80F48,
/// Maint asset weight to reach at _shift_end.
pub maint_weight_shift_asset_target: I80F48,
pub maint_weight_shift_liab_target: I80F48,
pub fallback_oracle: Pubkey, // unused, introduced in v0.22
/// Oracle that may be used if the main oracle is stale or not confident enough.
/// If this is Pubkey::default(), no fallback is available.
pub fallback_oracle: Pubkey,
/// zero means none, in token native
pub deposit_limit: u64,
pub reserved: [u8; 1968],
/// The unscaled borrow interest curve point for zero utilization.
///
/// See util0, rate0, util1, rate1, max_rate
pub zero_util_rate: I80F48,
/// Additional to liquidation_fee, but goes to the group owner instead of the liqor
pub platform_liquidation_fee: I80F48,
/// Platform fees that were collected during liquidation (in native tokens)
///
/// See also collected_fees_native and fees_withdrawn.
pub collected_liquidation_fees: I80F48,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 1920],
}
const_assert_eq!(
size_of::<Bank>(),
@ -209,7 +255,8 @@ const_assert_eq!(
+ 16 * 3
+ 32
+ 8
+ 1968
+ 16 * 3
+ 1920
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -252,6 +299,7 @@ impl Bank {
indexed_deposits: I80F48::ZERO,
indexed_borrows: I80F48::ZERO,
collected_fees_native: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO,
fees_withdrawn: 0,
dust: I80F48::ZERO,
flash_loan_approved_amount: 0,
@ -313,15 +361,18 @@ impl Bank {
maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_target,
fallback_oracle: existing_bank.oracle,
deposit_limit: existing_bank.deposit_limit,
reserved: [0; 1968],
zero_util_rate: existing_bank.zero_util_rate,
platform_liquidation_fee: existing_bank.platform_liquidation_fee,
reserved: [0; 1920],
}
}
pub fn verify(&self) -> Result<()> {
require_gte!(self.oracle_config.conf_filter, 0.0);
require_gte!(self.util0, I80F48::ZERO);
require_gte!(self.util1, self.util0);
require_gte!(I80F48::ONE, self.util1);
require_gte!(self.rate0, I80F48::ZERO);
require_gte!(self.util1, I80F48::ZERO);
require_gte!(self.rate1, I80F48::ZERO);
require_gte!(self.max_rate, I80F48::ZERO);
require_gte!(self.loan_fee_rate, 0.0);
@ -344,6 +395,8 @@ impl Bank {
require_gte!(self.maint_weight_shift_duration_inv, 0.0);
require_gte!(self.maint_weight_shift_asset_target, 0.0);
require_gte!(self.maint_weight_shift_liab_target, 0.0);
require_gte!(self.zero_util_rate, I80F48::ZERO);
require_gte!(self.platform_liquidation_fee, 0.0);
Ok(())
}
@ -989,6 +1042,7 @@ impl Bank {
pub fn compute_interest_rate(&self, utilization: I80F48) -> I80F48 {
Bank::interest_rate_curve_calculator(
utilization,
self.zero_util_rate,
self.util0,
self.rate0,
self.util1,
@ -1003,6 +1057,7 @@ impl Bank {
#[inline(always)]
pub fn interest_rate_curve_calculator(
utilization: I80F48,
zero_util_rate: I80F48,
util0: I80F48,
rate0: I80F48,
util1: I80F48,
@ -1014,8 +1069,8 @@ impl Bank {
let utilization = utilization.max(I80F48::ZERO).min(I80F48::ONE);
let v = if utilization <= util0 {
let slope = rate0 / util0;
slope * utilization
let slope = (rate0 - zero_util_rate) / util0;
zero_util_rate + slope * utilization
} else if utilization <= util1 {
let extra_util = utilization - util0;
let slope = (rate1 - rate0) / (util1 - util0);
@ -1083,19 +1138,52 @@ impl Bank {
self.interest_curve_scaling = (self.interest_curve_scaling * adjustment).max(1.0)
}
pub fn oracle_price(
/// Tries to return the primary oracle price, and if there is a confidence or staleness issue returns the fallback oracle price if possible.
pub fn oracle_price<T: KeyedAccountReader>(
&self,
oracle_acc: &impl KeyedAccountReader,
oracle_acc_infos: &OracleAccountInfos<T>,
staleness_slot: Option<u64>,
) -> Result<I80F48> {
require_keys_eq!(self.oracle, *oracle_acc.key());
let state = oracle::oracle_state_unchecked(oracle_acc, self.mint_decimals)?;
state.check_confidence_and_maybe_staleness(
&self.oracle,
&self.oracle_config,
staleness_slot,
)?;
Ok(state.price)
require_keys_eq!(self.oracle, *oracle_acc_infos.oracle.key());
let primary_state = oracle::oracle_state_unchecked(oracle_acc_infos, self.mint_decimals)?;
let primary_ok =
primary_state.check_confidence_and_maybe_staleness(&self.oracle_config, staleness_slot);
if primary_ok.is_oracle_error() && oracle_acc_infos.fallback_opt.is_some() {
let fallback_oracle_acc = oracle_acc_infos.fallback_opt.unwrap();
require_keys_eq!(self.fallback_oracle, *fallback_oracle_acc.key());
let fallback_state =
oracle::fallback_oracle_state_unchecked(&oracle_acc_infos, self.mint_decimals)?;
let fallback_ok = fallback_state
.check_confidence_and_maybe_staleness(&self.oracle_config, staleness_slot);
fallback_ok.with_context(|| {
format!(
"{} {}",
oracle_log_context(
self.name(),
&primary_state,
&self.oracle_config,
staleness_slot
),
oracle_log_context(
self.name(),
&fallback_state,
&self.oracle_config,
staleness_slot
)
)
})?;
Ok(fallback_state.price)
} else {
primary_ok.with_context(|| {
oracle_log_context(
self.name(),
&primary_state,
&self.oracle_config,
staleness_slot,
)
})?;
Ok(primary_state.price)
}
}
pub fn stable_price(&self) -> I80F48 {
@ -1589,4 +1677,95 @@ mod tests {
Ok(())
}
#[test]
pub fn test_bank_interest() -> Result<()> {
let index_start = I80F48::from(1_000_000);
let mut bank = Bank::zeroed();
bank.util0 = I80F48::from_num(0.5);
bank.rate0 = I80F48::from_num(0.02);
bank.util1 = I80F48::from_num(0.75);
bank.rate1 = I80F48::from_num(0.05);
bank.max_rate = I80F48::from_num(0.5);
bank.interest_curve_scaling = 4.0;
bank.deposit_index = index_start;
bank.borrow_index = index_start;
bank.net_borrow_limit_window_size_ts = 1;
let mut position0 = TokenPosition::default();
let mut position1 = TokenPosition::default();
// create 100% utilization, meaning 0.5 * 4 = 200% interest
bank.deposit(&mut position0, I80F48::from(1_000_000_000), 0)
.unwrap();
bank.withdraw_without_fee(&mut position1, I80F48::from(1_000_000_000), 0)
.unwrap();
// accumulate interest for a day at 5s intervals
let interval = 5;
for i in 0..24 * 60 * 60 / interval {
let (deposit_index, borrow_index, borrow_fees, borrow_rate, deposit_rate) = bank
.compute_index(
bank.indexed_deposits,
bank.indexed_borrows,
I80F48::from(interval),
)
.unwrap();
bank.deposit_index = deposit_index;
bank.borrow_index = borrow_index;
}
// the 5s rate is 2/(365*24*60*60/5), so
// expected is (1+five_sec_rate)^(24*60*60/5)
assert!(
((bank.deposit_index / index_start).to_num::<f64>() - 1.0054944908).abs() < 0.0000001
);
assert!(
((bank.borrow_index / index_start).to_num::<f64>() - 1.0054944908).abs() < 0.0000001
);
Ok(())
}
#[test]
fn test_bank_interest_rate_curve() {
let mut bank = Bank::zeroed();
bank.zero_util_rate = I80F48::from(1);
bank.rate0 = I80F48::from(3);
bank.rate1 = I80F48::from(7);
bank.max_rate = I80F48::from(13);
bank.util0 = I80F48::from_num(0.5);
bank.util1 = I80F48::from_num(0.75);
let interest = |v: f64| {
bank.compute_interest_rate(I80F48::from_num(v))
.to_num::<f64>()
};
let d = |a: f64, b: f64| (a - b).abs();
// the points
let eps = 0.0001;
assert!(d(interest(-0.5), 1.0) <= eps);
assert!(d(interest(0.0), 1.0) <= eps);
assert!(d(interest(0.5), 3.0) <= eps);
assert!(d(interest(0.75), 7.0) <= eps);
assert!(d(interest(1.0), 13.0) <= eps);
assert!(d(interest(1.5), 13.0) <= eps);
// midpoints
assert!(d(interest(0.25), 2.0) <= eps);
assert!(d(interest((0.5 + 0.75) / 2.0), 5.0) <= eps);
assert!(d(interest((0.75 + 1.0) / 2.0), 10.0) <= eps);
// around the points
let delta = 0.000001;
assert!(d(interest(0.0 + delta), 1.0) <= eps);
assert!(d(interest(0.5 - delta), 3.0) <= eps);
assert!(d(interest(0.5 + delta), 3.0) <= eps);
assert!(d(interest(0.75 - delta), 7.0) <= eps);
assert!(d(interest(0.75 + delta), 7.0) <= eps);
assert!(d(interest(1.0 - delta), 13.0) <= eps);
}
}

View File

@ -92,7 +92,10 @@ pub struct Group {
/// in seconds since epoch
pub fast_listing_interval_start: u64,
/// Number of fast listings that happened this interval
pub fast_listings_in_interval: u16,
/// Number of fast listings that are allowed per interval
pub allowed_fast_listings_per_interval: u16,
pub reserved: [u8; 1812],

View File

@ -850,18 +850,20 @@ impl<
&self,
market_index: PerpMarketIndex,
client_order_id: u64,
) -> Option<&PerpOpenOrder> {
self.all_perp_orders()
.find(|&oo| oo.is_active_for_market(market_index) && oo.client_id == client_order_id)
) -> Option<(usize, &PerpOpenOrder)> {
self.all_perp_orders().enumerate().find(|(_, &oo)| {
oo.is_active_for_market(market_index) && oo.client_id == client_order_id
})
}
pub fn perp_find_order_with_order_id(
&self,
market_index: PerpMarketIndex,
order_id: u128,
) -> Option<&PerpOpenOrder> {
) -> Option<(usize, &PerpOpenOrder)> {
self.all_perp_orders()
.find(|&oo| oo.is_active_for_market(market_index) && oo.id == order_id)
.enumerate()
.find(|(_, &oo)| oo.is_active_for_market(market_index) && oo.id == order_id)
}
pub fn being_liquidated(&self) -> bool {
@ -1200,62 +1202,49 @@ impl<
side: Side,
order_tree: BookSideOrderTree,
order: &LeafNode,
client_order_id: u64,
) -> Result<()> {
let mut perp_account = self.perp_position_mut(perp_market_index)?;
match side {
Side::Bid => {
perp_account.bids_base_lots += order.quantity;
}
Side::Ask => {
perp_account.asks_base_lots += order.quantity;
}
};
let perp_account = self.perp_position_mut(perp_market_index)?;
perp_account.adjust_maker_lots(side, order.quantity);
let slot = order.owner_slot as usize;
let mut oo = self.perp_order_mut_by_raw_index(slot);
let oo = self.perp_order_mut_by_raw_index(slot);
oo.market = perp_market_index;
oo.side_and_tree = SideAndOrderTree::new(side, order_tree).into();
oo.id = order.key;
oo.client_id = client_order_id;
oo.client_id = order.client_order_id;
oo.quantity = order.quantity;
Ok(())
}
/// Removes the perp order and updates the maker bids/asks tracking
///
/// The passed in `quantity` may differ from the quantity stored on the
/// perp open order slot, because maybe we're cancelling an order slot
/// for quantity 10 where 3 are in-flight in a FillEvent and 7 were left
/// on the book.
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
{
let oo = self.perp_order_mut_by_raw_index(slot);
require_neq!(oo.market, FREE_ORDER_SLOT);
let order_side = oo.side_and_tree().side();
let perp_market_index = oo.market;
let perp_account = self.perp_position_mut(perp_market_index)?;
let oo = self.perp_order_by_raw_index(slot)?;
require_neq!(oo.market, FREE_ORDER_SLOT);
let perp_market_index = oo.market;
let order_side = oo.side_and_tree().side();
// accounting
match order_side {
Side::Bid => {
perp_account.bids_base_lots -= quantity;
}
Side::Ask => {
perp_account.asks_base_lots -= quantity;
}
}
}
let perp_account = self.perp_position_mut(perp_market_index)?;
perp_account.adjust_maker_lots(order_side, -quantity);
// release space
let oo = self.perp_order_mut_by_raw_index(slot);
oo.market = FREE_ORDER_SLOT;
oo.side_and_tree = SideAndOrderTree::BidFixed.into();
oo.id = 0;
oo.client_id = 0;
oo.clear();
Ok(())
}
/// Returns amount of realized trade pnl for the maker
pub fn execute_perp_maker(
&mut self,
perp_market_index: PerpMarketIndex,
perp_market: &mut PerpMarket,
fill: &FillEvent,
group: &Group,
) -> Result<()> {
) -> Result<I80F48> {
let side = fill.taker_side().invert_side();
let (base_change, quote_change) = fill.base_quote_change(side);
let quote = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change);
@ -1269,31 +1258,47 @@ impl<
let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market);
pa.record_trading_fee(fees);
pa.record_trade(perp_market, base_change, quote);
let realized_pnl = pa.record_trade(perp_market, base_change, quote);
pa.maker_volume += quote.abs().to_num::<u64>();
if fill.maker_out() {
self.remove_perp_order(fill.maker_slot as usize, base_change.abs())
} else {
match side {
Side::Bid => {
pa.bids_base_lots -= base_change.abs();
}
Side::Ask => {
pa.asks_base_lots -= base_change.abs();
}
let quantity_filled = base_change.abs();
let maker_slot = fill.maker_slot as usize;
// Always adjust the bids/asks_base_lots for the filled amount.
// Because any early cancels only adjust it for the amount that was on the book,
// so even fill events that come after the slot was freed still need to clear
// the pending maker lots.
pa.adjust_maker_lots(side, -quantity_filled);
let oo = self.perp_order_mut_by_raw_index(maker_slot);
let is_active = oo.is_active_for_market(perp_market_index);
// Old fill events have no maker order id and match against any order.
// (this works safely because we don't allow old order's slots to be
// prematurely freed - and new orders can only have new fill events)
let is_old_fill = fill.maker_order_id == 0;
let order_id_match = is_old_fill || oo.id == fill.maker_order_id;
if is_active && order_id_match {
// Old orders have quantity=0
oo.quantity = (oo.quantity - quantity_filled).max(0);
if fill.maker_out() {
oo.clear();
}
Ok(())
}
Ok(realized_pnl)
}
/// Returns amount of realized trade pnl for the taker
pub fn execute_perp_taker(
&mut self,
perp_market_index: PerpMarketIndex,
perp_market: &mut PerpMarket,
fill: &FillEvent,
) -> Result<()> {
) -> Result<I80F48> {
let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market);
@ -1302,10 +1307,41 @@ impl<
// fees are assessed at time of trade; no need to assess fees here
let quote_change_native =
I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change);
pa.record_trade(perp_market, base_change, quote_change_native);
let realized_pnl = pa.record_trade(perp_market, base_change, quote_change_native);
pa.taker_volume += quote_change_native.abs().to_num::<u64>();
Ok(realized_pnl)
}
pub fn execute_perp_out_event(
&mut self,
perp_market_index: PerpMarketIndex,
side: Side,
slot: usize,
quantity: i64,
order_id: u128,
) -> Result<()> {
// Always free up the maker lots tracking, regardless of whether the
// order slot is still on the account or not
let pa = self.perp_position_mut(perp_market_index)?;
pa.adjust_maker_lots(side, -quantity);
let oo = self.perp_order_mut_by_raw_index(slot);
let is_active = oo.is_active_for_market(perp_market_index);
// Old events have no order id and match against any order.
// (this works safely because we don't allow old order's slots to be
// prematurely freed - and new orders can only have new events)
let is_old_event = order_id == 0;
let order_id_match = is_old_event || oo.id == order_id;
// This may be a delayed out event (slot may be empty or reused), so make
// sure it's the right one before canceling.
if is_active && order_id_match {
oo.clear();
}
Ok(())
}
@ -1814,8 +1850,11 @@ impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAcc
#[cfg(test)]
mod tests {
use bytemuck::Zeroable;
use itertools::Itertools;
use crate::state::PostOrderType;
use super::*;
fn make_test_account() -> MangoAccountValue {
@ -2561,4 +2600,212 @@ mod tests {
}
Ok(())
}
#[test]
fn test_perp_order_events() -> Result<()> {
let group = Group::zeroed();
let perp_market_index = 0;
let mut perp_market = PerpMarket::zeroed();
let mut account = make_test_account();
account.ensure_token_position(0)?;
account.ensure_perp_position(perp_market_index, 0)?;
let owner = Pubkey::new_unique();
let slot = account.perp_next_order_slot()?;
let order_id = 127;
let quantity = 42;
let order = LeafNode::new(
slot as u8,
order_id,
owner,
quantity,
1,
PostOrderType::Limit,
0,
0,
0,
);
let side = Side::Bid;
account.add_perp_order(0, side, BookSideOrderTree::Fixed, &order)?;
let make_fill = |quantity, out, order_id| {
FillEvent::new(
side.invert_side(),
out,
slot as u8,
0,
0,
owner,
order_id,
0,
I80F48::ZERO,
0,
owner,
0,
I80F48::ZERO,
1,
quantity,
)
};
let pp = |a: &MangoAccountValue| a.perp_position(perp_market_index).unwrap().clone();
{
// full fill
let mut account = account.clone();
let fill = make_fill(quantity, true, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// full fill, no order id
let mut account = account.clone();
let fill = make_fill(quantity, true, 0);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// out event
let mut account = account.clone();
account.execute_perp_out_event(perp_market_index, side, slot, quantity, order_id)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// out event, no order id
let mut account = account.clone();
account.execute_perp_out_event(perp_market_index, side, slot, quantity, 0)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// cancel
let mut account = account.clone();
account.remove_perp_order(slot, quantity)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// partial fill event, user closes rest, following out event has no effect
let mut account = account.clone();
let fill = make_fill(quantity - 10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, 10);
// out event happens but is delayed
account.remove_perp_order(slot, 0)?;
assert_eq!(pp(&account).bids_base_lots, 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
account.execute_perp_out_event(perp_market_index, side, slot, 10, order_id)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
}
{
// partial fill and out are delayed, user closes first
let mut account = account.clone();
account.remove_perp_order(slot, 0)?;
assert_eq!(pp(&account).bids_base_lots, quantity);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
let fill = make_fill(quantity - 10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 10);
assert_eq!(pp(&account).asks_base_lots, 0);
account.execute_perp_out_event(perp_market_index, side, slot, 10, order_id)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
}
{
// partial fill and cancel, cancel before outevent
let mut account = account.clone();
account.remove_perp_order(slot, 10)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
let fill = make_fill(quantity - 10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
}
{
// several fills
let mut account = account.clone();
let fill = make_fill(10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(
account.perp_order_by_raw_index(slot)?.quantity,
quantity - 10
);
let fill = make_fill(10, false, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 20);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(
account.perp_order_by_raw_index(slot)?.quantity,
quantity - 20
);
let fill = make_fill(quantity - 20, true, order_id);
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, 0);
assert_eq!(pp(&account).asks_base_lots, 0);
assert!(!account.perp_order_by_raw_index(0)?.is_active());
}
{
// mismatched fill and out
let mut account = account.clone();
let mut fill = make_fill(10, false, order_id);
fill.maker_order_id = 1;
account.execute_perp_maker(perp_market_index, &mut perp_market, &fill, &group)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 10);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, quantity);
account.execute_perp_out_event(perp_market_index, side, slot, 10, 1)?;
assert_eq!(pp(&account).bids_base_lots, quantity - 20);
assert_eq!(pp(&account).asks_base_lots, 0);
assert_eq!(account.perp_order_by_raw_index(slot)?.quantity, quantity);
}
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -33,8 +33,10 @@ pub struct MintInfo {
pub registration_time: u64,
pub fallback_oracle: Pubkey,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 2560],
pub reserved: [u8; 2528],
}
const_assert_eq!(
size_of::<MintInfo>(),

View File

@ -7,6 +7,7 @@ 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::*;
@ -22,6 +23,7 @@ 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

@ -1,9 +1,9 @@
use std::mem::size_of;
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;
use anchor_lang::{AnchorDeserialize, Discriminator};
use derivative::Derivative;
use fixed::types::I80F48;
use fixed::types::{I80F48, U64F64};
use static_assertions::const_assert_eq;
use switchboard_program::FastRoundResultAccountData;
@ -12,6 +12,9 @@ use switchboard_v2::AggregatorAccountData;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::load_whirlpool_state;
use super::orca_mainnet_whirlpool;
const DECIMAL_CONSTANT_ZERO_INDEX: i8 = 12;
const DECIMAL_CONSTANTS: [I80F48; 25] = [
@ -46,6 +49,7 @@ pub const fn power_of_ten(decimals: i8) -> I80F48 {
}
pub const QUOTE_DECIMALS: i8 = 6;
pub const SOL_DECIMALS: i8 = 9;
pub const QUOTE_NATIVE_TO_UI: I80F48 = power_of_ten(-QUOTE_DECIMALS);
pub mod switchboard_v1_devnet_oracle {
@ -57,6 +61,26 @@ pub mod switchboard_v2_mainnet_oracle {
declare_id!("DtmE9D2CSB4L5D6A15mraeEjrGMm6auWVzgaD8hK2tZM");
}
pub mod pyth_mainnet_usdc_oracle {
use solana_program::declare_id;
declare_id!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");
}
pub mod pyth_mainnet_sol_oracle {
use solana_program::declare_id;
declare_id!("H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG");
}
pub mod usdc_mint_mainnet {
use solana_program::declare_id;
declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
}
pub mod sol_mint_mainnet {
use solana_program::declare_id;
declare_id!("So11111111111111111111111111111111111111112");
}
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
#[derivative(Debug)]
@ -92,6 +116,7 @@ pub enum OracleType {
Stub,
SwitchboardV1,
SwitchboardV2,
OrcaCLMM,
}
pub struct OracleState {
@ -105,49 +130,29 @@ impl OracleState {
#[inline]
pub fn check_confidence_and_maybe_staleness(
&self,
oracle_pk: &Pubkey,
config: &OracleConfig,
staleness_slot: Option<u64>,
) -> Result<()> {
if let Some(now_slot) = staleness_slot {
self.check_staleness(oracle_pk, config, now_slot)?;
self.check_staleness(config, now_slot)?;
}
self.check_confidence(oracle_pk, config)
self.check_confidence(config)
}
pub fn check_staleness(
&self,
oracle_pk: &Pubkey,
config: &OracleConfig,
now_slot: u64,
) -> Result<()> {
pub fn check_staleness(&self, config: &OracleConfig, now_slot: u64) -> Result<()> {
if config.max_staleness_slots >= 0
&& self
.last_update_slot
.saturating_add(config.max_staleness_slots as u64)
< now_slot
{
msg!(
"Oracle is stale; pubkey {}, price: {}, last_update_slot: {}, now_slot: {}",
oracle_pk,
self.price.to_num::<f64>(),
self.last_update_slot,
now_slot,
);
return Err(MangoError::OracleStale.into());
}
Ok(())
}
pub fn check_confidence(&self, oracle_pk: &Pubkey, config: &OracleConfig) -> Result<()> {
pub fn check_confidence(&self, config: &OracleConfig) -> Result<()> {
if self.deviation > config.conf_filter * self.price {
msg!(
"Oracle confidence not good enough: pubkey {}, price: {}, deviation: {}, conf_filter: {}",
oracle_pk,
self.price.to_num::<f64>(),
self.deviation.to_num::<f64>(),
config.conf_filter.to_num::<f32>(),
);
return Err(MangoError::OracleConfidence.into());
}
Ok(())
@ -188,11 +193,33 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|| acc_info.owner() == &switchboard_v2_mainnet_oracle::ID
{
return Ok(OracleType::SwitchboardV1);
} else if acc_info.owner() == &orca_mainnet_whirlpool::ID {
return Ok(OracleType::OrcaCLMM);
}
Err(MangoError::UnknownOracleType.into())
}
pub fn check_is_valid_fallback_oracle(acc_info: &impl KeyedAccountReader) -> Result<()> {
if acc_info.key() == &Pubkey::default() {
return Ok(());
};
let oracle_type = determine_oracle_type(acc_info)?;
if oracle_type == OracleType::OrcaCLMM {
let whirlpool = load_whirlpool_state(acc_info)?;
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
);
}
Ok(())
}
/// Get the pyth agg price if it's available, otherwise take the prev price.
///
/// Returns the publish slot in addition to the price info.
@ -226,24 +253,86 @@ fn pyth_get_price(
}
}
fn get_pyth_state(
acc_info: &(impl KeyedAccountReader + ?Sized),
base_decimals: u8,
) -> Result<OracleState> {
let data = &acc_info.data();
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
let (price_data, last_update_slot) = pyth_get_price(acc_info.key(), price_account);
let decimals = (price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8);
let decimal_adj = power_of_ten(decimals);
let price = I80F48::from_num(price_data.price) * decimal_adj;
let deviation = I80F48::from_num(price_data.conf) * decimal_adj;
require_gte!(price, 0);
Ok(OracleState {
price,
last_update_slot,
deviation,
oracle_type: OracleType::Pyth,
})
}
/// Contains all oracle account infos that could be used to read price
pub struct OracleAccountInfos<'a, T: KeyedAccountReader> {
pub oracle: &'a T,
pub fallback_opt: Option<&'a T>,
pub usd_opt: Option<&'a T>,
pub sol_opt: Option<&'a T>,
}
impl<'a, T: KeyedAccountReader> OracleAccountInfos<'a, T> {
pub fn from_reader(acc_reader: &'a T) -> Self {
OracleAccountInfos {
oracle: acc_reader,
fallback_opt: None,
usd_opt: None,
sol_opt: None,
}
}
}
/// Returns the price of one native base token, in native quote tokens
///
/// Example: The for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native)
/// Example: The price for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native)
///
/// This currently assumes that quote decimals (i.e. decimals for USD) is 6, like for USDC.
///
/// The staleness and confidence of the oracle is not checked. Use the functions on
/// OracleState to validate them if needed. That's why this function is called _unchecked.
pub fn oracle_state_unchecked(
acc_info: &impl KeyedAccountReader,
pub fn oracle_state_unchecked<T: KeyedAccountReader>(
acc_infos: &OracleAccountInfos<T>,
base_decimals: u8,
) -> Result<OracleState> {
let data = &acc_info.data();
let oracle_type = determine_oracle_type(acc_info)?;
oracle_state_unchecked_inner(acc_infos, base_decimals, false)
}
pub fn fallback_oracle_state_unchecked<T: KeyedAccountReader>(
acc_infos: &OracleAccountInfos<T>,
base_decimals: u8,
) -> Result<OracleState> {
oracle_state_unchecked_inner(acc_infos, base_decimals, true)
}
fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
acc_infos: &OracleAccountInfos<T>,
base_decimals: u8,
use_fallback: bool,
) -> Result<OracleState> {
let oracle_info = if use_fallback {
acc_infos
.fallback_opt
.ok_or_else(|| error!(MangoError::UnknownOracleType))?
} else {
acc_infos.oracle
};
let data = &oracle_info.data();
let oracle_type = determine_oracle_type(oracle_info)?;
Ok(match oracle_type {
OracleType::Stub => {
let stub = acc_info.load::<StubOracle>()?;
let stub = oracle_info.load::<StubOracle>()?;
let deviation = if stub.deviation == 0 {
// allows the confidence check to pass even for negative prices
I80F48::MIN
@ -263,22 +352,7 @@ pub fn oracle_state_unchecked(
oracle_type: OracleType::Stub,
}
}
OracleType::Pyth => {
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
let (price_data, last_update_slot) = pyth_get_price(acc_info.key(), price_account);
let decimals = (price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8);
let decimal_adj = power_of_ten(decimals);
let price = I80F48::from_num(price_data.price) * decimal_adj;
let deviation = I80F48::from_num(price_data.conf) * decimal_adj;
require_gte!(price, 0);
OracleState {
price,
last_update_slot,
deviation,
oracle_type: OracleType::Pyth,
}
}
OracleType::Pyth => get_pyth_state(oracle_info, base_decimals)?,
OracleType::SwitchboardV2 => {
fn from_foreign_error(e: impl std::fmt::Display) -> Error {
error_msg!("{}", e)
@ -329,9 +403,76 @@ pub fn oracle_state_unchecked(
oracle_type: OracleType::SwitchboardV1,
}
}
OracleType::OrcaCLMM => {
let whirlpool = load_whirlpool_state(oracle_info)?;
let inverted = whirlpool.token_mint_a == usdc_mint_mainnet::ID
|| (whirlpool.token_mint_a == sol_mint_mainnet::ID
&& whirlpool.token_mint_b != usdc_mint_mainnet::ID);
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;
OracleState {
price,
last_update_slot: quote_state.last_update_slot,
deviation: quote_state.deviation,
oracle_type: OracleType::OrcaCLMM,
}
}
})
}
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
.usd_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,
oracle_config: &OracleConfig,
staleness_slot: Option<u64>,
) -> String {
format!(
"name: {}, price: {}, deviation: {}, last_update_slot: {}, now_slot: {}, conf_filter: {:#?}",
name,
state.price.to_num::<f64>(),
state.deviation.to_num::<f64>(),
state.last_update_slot,
staleness_slot.unwrap_or_else(|| u64::MAX),
oracle_config.conf_filter.to_num::<f32>(),
)
}
#[cfg(test)]
mod tests {
use super::*;
@ -360,6 +501,11 @@ mod tests {
OracleType::SwitchboardV2,
Pubkey::default(),
),
(
"83v8iPyZihDEjDdY8RdZddyZNyUtXngz69Lgo9Kt5d6d",
OracleType::OrcaCLMM,
orca_mainnet_whirlpool::ID,
),
];
for fixture in fixtures {
@ -399,4 +545,103 @@ mod tests {
)
}
}
#[test]
pub fn test_clmm_price() -> 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
),
(
"Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD",
OracleType::Pyth,
Pubkey::default(),
6,
),
];
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,
usd_opt: None,
sol_opt: None,
};
let orca_ais = OracleAccountInfos {
oracle: ai,
fallback_opt: None,
usd_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());
let data = RefCell::new(&mut clmm_data[..]);
let ai = &AccountInfoRef {
key: &Pubkey::from_str(fixture.0).unwrap(),
owner: &fixture.2,
data: data.borrow(),
};
let base_decimals = fixture.3;
assert!(determine_oracle_type(ai).unwrap() == fixture.1);
let oracle_infos = OracleAccountInfos {
oracle: ai,
fallback_opt: None,
usd_opt: None,
sol_opt: None,
};
assert!(oracle_state_unchecked(&oracle_infos, base_decimals)
.is_anchor_error_with_code(6068));
}
Ok(())
}
}

View File

@ -0,0 +1,48 @@
use anchor_lang::prelude::*;
use solana_program::pubkey::Pubkey;
use crate::{accounts_zerocopy::KeyedAccountReader, error::MangoError};
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
}
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

@ -1,6 +1,8 @@
use crate::error::*;
use crate::logs::{emit_stack, FilledPerpOrderLog, PerpTakerTradeLog};
use crate::state::{orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket};
use crate::state::{
orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket, PerpMarketIndex,
};
use anchor_lang::prelude::*;
use bytemuck::cast;
use fixed::types::I80F48;
@ -91,13 +93,11 @@ impl<'a> Orderbook<'a> {
// Remove the order from the book unless we've done that enough
if number_of_dropped_expired_orders < DROP_EXPIRED_ORDER_LIMIT {
number_of_dropped_expired_orders += 1;
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
other_side,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.node.owner,
best_opposing.node.quantity,
best_opposing.node,
);
event_queue.push_back(cast(event)).unwrap();
orders_to_delete
@ -140,13 +140,11 @@ impl<'a> Orderbook<'a> {
decremented_base_lots += match_base_lots;
}
SelfTradeBehavior::CancelProvide => {
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
other_side,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
best_opposing.node.owner,
best_opposing.node.quantity,
best_opposing.node,
);
event_queue.push_back(cast(event)).unwrap();
orders_to_delete
@ -181,6 +179,7 @@ impl<'a> Orderbook<'a> {
now_ts,
seq_num,
best_opposing.node.owner,
best_opposing.node.key,
best_opposing.node.client_order_id,
if order_would_self_trade {
I80F48::ZERO
@ -272,13 +271,11 @@ impl<'a> Orderbook<'a> {
// Drop an expired order if possible
if let Some(expired_order) = bookside.remove_one_expired(order_tree_target, now_ts) {
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
side,
expired_order.owner_slot,
now_ts,
event_queue.header.seq_num,
expired_order.owner,
expired_order.quantity,
&expired_order,
);
event_queue.push_back(cast(event)).unwrap();
}
@ -292,13 +289,11 @@ impl<'a> Orderbook<'a> {
side.is_price_better(price_lots, worst_price),
MangoError::SomeError
);
let event = OutEvent::new(
let event = OutEvent::from_leaf_node(
side,
worst_order.owner_slot,
now_ts,
event_queue.header.seq_num,
worst_order.owner,
worst_order.quantity,
&worst_order,
);
event_queue.push_back(cast(event)).unwrap();
}
@ -334,7 +329,6 @@ impl<'a> Orderbook<'a> {
side,
order_tree_target,
&new_order,
order.client_order_id,
)?;
}
@ -351,6 +345,7 @@ impl<'a> Orderbook<'a> {
pub fn cancel_all_orders(
&mut self,
mango_account: &mut MangoAccountRefMut,
mango_account_pk: &Pubkey,
perp_market: &mut PerpMarket,
mut limit: u8,
side_to_cancel_option: Option<Side>,
@ -371,8 +366,12 @@ impl<'a> Orderbook<'a> {
let order_id = oo.id;
let cancel_result =
self.cancel_order(mango_account, order_id, order_side_and_tree, None);
let cancel_result = self.cancel_order_by_slot(
mango_account,
mango_account_pk,
i,
perp_market.perp_market_index,
);
if cancel_result.is_anchor_error_with_code(MangoError::PerpOrderIdNotFound.into()) {
// It's possible for the order to be filled or expired already.
// There will be an event on the queue, the perp order slot is freed once
@ -394,8 +393,53 @@ impl<'a> Orderbook<'a> {
Ok(())
}
/// Cancels an order in an open order slot, removing it from open orders list
/// and from the orderbook (unless already filled/expired)
pub fn cancel_order_by_slot(
&mut self,
mango_account: &mut MangoAccountRefMut,
mango_account_pk: &Pubkey,
slot: usize,
perp_market_index: PerpMarketIndex,
) -> Result<()> {
let oo = mango_account.perp_order_by_raw_index(slot)?;
if !oo.is_active_for_market(perp_market_index) {
return Err(error_msg_typed!(
MangoError::SomeError,
"perp orders at slot {slot} is not active for perp market {perp_market_index}"
));
}
let side_and_tree = oo.side_and_tree();
let side = side_and_tree.side();
let book_component = side_and_tree.order_tree();
let order_id = oo.id;
let leaf_node_opt = self
.bookside_mut(side)
.remove_by_key(book_component, order_id);
// If the order is still on the book, cancel it without an OutEvent and free up the order
// quantity immediately. If it's not on the book, the OutEvent or FillEvent is responsible
// for freeing up quantity, even if we already free up the slot itself here.
let on_book_quantity = if let Some(leaf_node) = leaf_node_opt {
require_eq!(leaf_node.owner_slot as usize, slot);
require_keys_eq!(leaf_node.owner, *mango_account_pk);
leaf_node.quantity
} else {
// Old orders didn't keep track of `quantity` on the oo slot. They are not allowed
// to be cancelled while a canceling Fill- or OutEvent is in flight.
if oo.quantity == 0 {
return Err(error_msg_typed!(MangoError::PerpOrderIdNotFound, "no perp order with id {order_id}, side {side:?}, component {book_component:?} found on the orderbook"));
}
0
};
mango_account.remove_perp_order(slot, on_book_quantity)?;
Ok(())
}
/// Cancels an order on a side, removing it from the book and the mango account orders list
pub fn cancel_order(
pub fn cancel_order_by_id(
&mut self,
mango_account: &mut MangoAccountRefMut,
order_id: u128,

View File

@ -180,6 +180,58 @@ impl BookSide {
}
None
}
/// Walk up the book given base units and return the amount in quote lots an order would
/// be filled at. If not enough liquidity is on book, return None
pub fn matched_amount(
&self,
quantity: i64,
now_ts: u64,
oracle_price_lots: i64,
) -> Option<i64> {
if quantity <= 0 {
return None;
}
let mut sum_qty: i64 = 0;
let mut sum_amt: i64 = 0;
for order in self.iter_valid(now_ts, oracle_price_lots) {
sum_qty += order.node.quantity;
sum_amt += order.node.quantity * order.price_lots;
let extra_qty = sum_qty - quantity;
if extra_qty >= 0 {
sum_amt -= extra_qty * order.price_lots;
return Some(sum_amt);
}
}
None
}
/// Walk up the book given quote units and return the quantity in base lots
/// an order would need to request to match at least the requested amount.
/// If not enough liquidity is on book, return None
pub fn matched_quantity(
&self,
amount: i64,
now_ts: u64,
oracle_price_lots: i64,
) -> Option<i64> {
if amount <= 0 {
return None;
}
let mut sum_qty: i64 = 0;
let mut sum_amt: i64 = 0;
for order in self.iter_valid(now_ts, oracle_price_lots) {
sum_qty += order.node.quantity;
sum_amt += order.node.quantity * order.price_lots;
let extra_amt = sum_amt - amount;
if extra_amt >= 0 {
// adding n-1 before dividing through n to force rounding up
sum_qty -= (extra_amt + order.price_lots - 1) / order.price_lots;
return Some(sum_qty);
}
}
None
}
}
#[cfg(test)]

View File

@ -6,7 +6,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use std::mem::size_of;
use super::Side;
use super::{LeafNode, Side};
pub const MAX_NUM_EVENTS: u32 = 488;
@ -154,7 +154,9 @@ impl QueueHeader for EventQueueHeader {
}
}
#[allow(dead_code)]
const EVENT_SIZE: usize = 208;
#[zero_copy]
#[derive(Debug)]
pub struct AnyEvent {
@ -194,7 +196,7 @@ pub struct FillEvent {
pub taker: Pubkey,
pub padding3: [u8; 16],
pub taker_client_order_id: u64,
pub padding4: [u8; 16],
pub maker_order_id: u128,
pub price: i64,
pub quantity: i64, // number of quote lots
@ -215,6 +217,7 @@ impl FillEvent {
timestamp: u64,
seq_num: u64,
maker: Pubkey,
maker_order_id: u128,
maker_client_order_id: u64,
maker_fee: I80F48,
maker_timestamp: u64,
@ -232,6 +235,7 @@ impl FillEvent {
timestamp,
seq_num,
maker,
maker_order_id,
maker_client_order_id,
maker_fee: maker_fee.to_num::<f32>(),
maker_timestamp,
@ -243,7 +247,6 @@ impl FillEvent {
padding: Default::default(),
padding2: Default::default(),
padding3: Default::default(),
padding4: Default::default(),
reserved: [0; 8],
}
}
@ -306,7 +309,8 @@ pub struct OutEvent {
pub seq_num: u64,
pub owner: Pubkey,
pub quantity: i64,
padding1: [u8; 144],
pub order_id: u128,
padding1: [u8; 128],
}
const_assert_eq!(size_of::<OutEvent>() % 8, 0);
const_assert_eq!(size_of::<OutEvent>(), EVENT_SIZE);
@ -319,6 +323,7 @@ impl OutEvent {
seq_num: u64,
owner: Pubkey,
quantity: i64,
order_id: u128,
) -> Self {
Self {
event_type: EventType::Out.into(),
@ -329,10 +334,23 @@ impl OutEvent {
seq_num,
owner,
quantity,
padding1: [0; EVENT_SIZE - 64],
order_id,
padding1: [0; 128],
}
}
pub fn from_leaf_node(side: Side, timestamp: u64, seq_num: u64, node: &LeafNode) -> Self {
Self::new(
side,
node.owner_slot,
timestamp,
seq_num,
node.owner,
node.quantity,
node.key,
)
}
pub fn side(&self) -> Side {
self.side.try_into().unwrap()
}

View File

@ -4,16 +4,20 @@ use anchor_lang::prelude::*;
use derivative::Derivative;
use fixed::types::I80F48;
use oracle::oracle_log_context;
use static_assertions::const_assert_eq;
use crate::accounts_zerocopy::KeyedAccountReader;
use crate::error::MangoError;
use crate::error::{Contextable, MangoError};
use crate::logs::{emit_stack, PerpUpdateFundingLogV2};
use crate::state::orderbook::Side;
use crate::state::{oracle, TokenIndex};
use crate::util;
use super::{orderbook, OracleConfig, OracleState, Orderbook, StablePriceModel, DAY_I80F48};
use super::{
orderbook, OracleAccountInfos, OracleConfig, OracleState, Orderbook, StablePriceModel,
DAY_I80F48,
};
pub type PerpMarketIndex = u16;
@ -185,8 +189,17 @@ pub struct PerpMarket {
// This ensures that fees_settled is strictly increasing for stats gathering purposes
pub fees_withdrawn: u64,
/// Additional to liquidation_fee, but goes to the group owner instead of the liqor
pub platform_liquidation_fee: I80F48,
/// Platform fees that were accrued during liquidation (in native tokens)
///
/// These fees are also added to fees_accrued, this is just for bookkeeping the total
/// liquidation fees that happened. So never decreases (different to fees_accrued).
pub accrued_liquidation_fees: I80F48,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 1880],
pub reserved: [u8; 1848],
}
const_assert_eq!(
@ -223,7 +236,8 @@ const_assert_eq!(
+ 7
+ 3 * 16
+ 8
+ 1880
+ 2 * 16
+ 1848
);
const_assert_eq!(size_of::<PerpMarket>(), 2808);
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
@ -260,26 +274,26 @@ impl PerpMarket {
orderbook::new_node_key(side, price_data, self.seq_num)
}
pub fn oracle_price(
pub fn oracle_price<T: KeyedAccountReader>(
&self,
oracle_acc: &impl KeyedAccountReader,
oracle_acc_infos: &OracleAccountInfos<T>,
staleness_slot: Option<u64>,
) -> Result<I80F48> {
Ok(self.oracle_state(oracle_acc, staleness_slot)?.price)
Ok(self.oracle_state(oracle_acc_infos, staleness_slot)?.price)
}
pub fn oracle_state(
pub fn oracle_state<T: KeyedAccountReader>(
&self,
oracle_acc: &impl KeyedAccountReader,
oracle_acc_infos: &OracleAccountInfos<T>,
staleness_slot: Option<u64>,
) -> Result<OracleState> {
require_keys_eq!(self.oracle, *oracle_acc.key());
let state = oracle::oracle_state_unchecked(oracle_acc, self.base_decimals)?;
state.check_confidence_and_maybe_staleness(
&self.oracle,
&self.oracle_config,
staleness_slot,
)?;
require_keys_eq!(self.oracle, *oracle_acc_infos.oracle.key());
let state = oracle::oracle_state_unchecked(oracle_acc_infos, self.base_decimals)?;
state
.check_confidence_and_maybe_staleness(&self.oracle_config, staleness_slot)
.with_context(|| {
oracle_log_context(self.name(), &state, &self.oracle_config, staleness_slot)
})?;
Ok(state)
}
@ -521,7 +535,9 @@ impl PerpMarket {
init_overall_asset_weight: I80F48::ONE,
positive_pnl_liquidation_fee: I80F48::ZERO,
fees_withdrawn: 0,
reserved: [0; 1880],
platform_liquidation_fee: I80F48::ZERO,
accrued_liquidation_fees: I80F48::ZERO,
reserved: [0; 1848],
}
}
}

View File

@ -179,9 +179,10 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
.await
.unwrap();
assert!(account_position_closed(solana, account, collateral_token1.bank).await);
let liq_fee_factor = 1.02 * 1.02;
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
(-350.0f64 + (1000.0 / 20.0 / 1.02)).round() as i64
(-350.0f64 + (1000.0 / 20.0 / liq_fee_factor)).round() as i64
);
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
@ -203,7 +204,8 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
.await
.unwrap();
assert!(account_position_closed(solana, account, collateral_token2.bank).await);
let borrow1_after_liq = -350.0f64 + (1000.0 / 20.0 / 1.02) + (20.0 / 20.0 / 1.02);
let borrow1_after_liq =
-350.0f64 + (1000.0 / 20.0 / liq_fee_factor) + (20.0 / 20.0 / liq_fee_factor);
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
borrow1_after_liq.round() as i64

View File

@ -286,6 +286,7 @@ async fn test_basic() -> Result<(), TransportError> {
send_tx(
solana,
StubOracleCloseInstruction {
oracle: tokens[0].oracle,
group,
mint: bank_data.mint,
admin,
@ -459,6 +460,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
group,
admin,
mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
maint_weight_shift_start_opt: Some(start_time + 1000),
maint_weight_shift_end_opt: Some(start_time + 2000),
@ -492,6 +494,7 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
group,
admin,
mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
maint_weight_shift_abort: true,
..token_edit_instruction_default()
@ -563,6 +566,7 @@ async fn test_bank_deposit_limit() -> Result<(), TransportError> {
group,
admin,
mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
deposit_limit_opt: Some(2000),
..token_edit_instruction_default()

View File

@ -16,7 +16,7 @@ async fn test_delegate() -> Result<(), TransportError> {
// SETUP: Create a group, register a token (mint0), create an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
let GroupWithTokens { group, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
@ -24,7 +24,6 @@ async fn test_delegate() -> Result<(), TransportError> {
}
.create(solana)
.await;
let bank = tokens[0].bank;
let account =
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 100, 0).await;
@ -86,14 +85,14 @@ async fn test_delegate() -> Result<(), TransportError> {
}
//
// TEST: Close account as delegate should fail
// TEST: Withdrawing a tiny amount as delegate should be ok
//
{
let bank_data: Bank = solana.get_account(bank).await;
// withdraw most
send_tx(
solana,
TokenWithdrawInstruction {
amount: bank_data.native_deposits().to_num(),
amount: 99,
allow_borrow: false,
account,
owner,
@ -103,6 +102,26 @@ async fn test_delegate() -> Result<(), TransportError> {
)
.await
.unwrap();
send_tx(
solana,
TokenWithdrawInstruction {
amount: u64::MAX,
allow_borrow: false,
account,
owner: delegate,
token_account: context.users[0].token_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
}
//
// TEST: Close account as delegate should fail
//
{
let res = send_tx(
solana,
AccountCloseInstruction {

View File

@ -1,4 +1,5 @@
use super::*;
use anchor_lang::prelude::AccountMeta;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
async fn deposit_cu_datapoint(
@ -24,6 +25,31 @@ async fn deposit_cu_datapoint(
result.metadata.unwrap().compute_units_consumed
}
async fn deposit_cu_fallbacks_datapoint(
solana: &SolanaCookie,
account: Pubkey,
owner: TestKeypair,
token_account: Pubkey,
remaining_accounts: Vec<AccountMeta>,
) -> u64 {
let result = send_tx_with_extra_accounts(
solana,
TokenDepositInstruction {
amount: 10,
reduce_only: false,
account,
owner,
token_account,
token_authority: owner,
bank_index: 0,
},
remaining_accounts,
)
.await
.unwrap();
result.metadata.unwrap().compute_units_consumed
}
// Try to reach compute limits in health checks by having many different tokens in an account
#[tokio::test]
async fn test_health_compute_tokens() -> Result<(), TransportError> {
@ -68,7 +94,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::<u64>()
/ (cu_measurements.len() - 1) as u64;
println!("average cu increase: {avg_cu_increase}");
assert!(avg_cu_increase < 3200);
assert!(avg_cu_increase < 3350);
Ok(())
}
@ -107,6 +133,7 @@ async fn test_health_compute_tokens_during_maint_weight_shift() -> Result<(), Tr
group,
admin,
mint: mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
maint_weight_shift_start_opt: Some(now - 1000),
maint_weight_shift_end_opt: Some(now + 1000),
@ -137,7 +164,178 @@ async fn test_health_compute_tokens_during_maint_weight_shift() -> Result<(), Tr
let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::<u64>()
/ (cu_measurements.len() - 1) as u64;
println!("average cu increase: {avg_cu_increase}");
assert!(avg_cu_increase < 4200);
assert!(avg_cu_increase < 4300);
Ok(())
}
// Try to reach compute limits in health checks by having many different tokens in an account and using fallback oracles for them
#[tokio::test]
async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(450_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let num_tokens = 8;
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..num_tokens];
let mut fallback_oracle_kps = Vec::with_capacity(num_tokens);
for _ in 0..num_tokens {
fallback_oracle_kps.push(TestKeypair::new());
}
let success_metas: Vec<AccountMeta> = fallback_oracle_kps
.iter()
.map(|x| AccountMeta {
pubkey: x.pubkey(),
is_signer: false,
is_writable: false,
})
.collect();
let failure_metas = vec![];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let account =
create_funded_account(&solana, group, owner, 0, &context.users[1], &[], 1000, 0).await;
let mut success_measurements = vec![];
let mut failure_measurements = vec![];
for token_account in &context.users[0].token_accounts[..mints.len()] {
deposit_cu_datapoint(solana, account, owner, *token_account).await;
}
//
// SETUP: Create and register fallback oracles for each token
//
for (i, _token_account) in context.users[0].token_accounts[..mints.len()]
.iter()
.enumerate()
{
send_tx(
solana,
StubOracleCreate {
oracle: fallback_oracle_kps[i],
group,
mint: mints[i].pubkey,
admin,
payer,
},
)
.await
.unwrap();
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[i].pubkey,
fallback_oracle: fallback_oracle_kps[i].pubkey(),
options: mango_v4::instruction::TokenEdit {
set_fallback_oracle: true,
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
}
//
// TEST: Progressively make each oracle invalid so that the fallback is used
//
for (i, token_account) in context.users[0].token_accounts[..mints.len()]
.iter()
.enumerate()
{
send_tx(
solana,
StubOracleSetTestInstruction {
oracle: tokens[i].oracle,
group,
mint: mints[i].pubkey,
admin,
price: 1.0,
last_update_slot: 0,
deviation: 100.0,
},
)
.await
.unwrap();
success_measurements.push(
deposit_cu_fallbacks_datapoint(
solana,
account,
owner,
*token_account,
success_metas.clone(),
)
.await,
);
failure_measurements.push(
deposit_cu_fallbacks_datapoint(
solana,
account,
owner,
*token_account,
failure_metas.clone(),
)
.await,
);
}
println!("successful fallbacks:");
for (i, pair) in success_measurements.windows(2).enumerate() {
println!(
"after adding token {}: {} (+{})",
i,
pair[1],
pair[1] - pair[0]
);
}
println!("failed fallbacks:");
for (i, pair) in failure_measurements.windows(2).enumerate() {
println!(
"after adding token {}: {} (+{})",
i,
pair[1],
pair[1] - pair[0]
);
}
let avg_success_increase = success_measurements
.windows(2)
.map(|p| p[1] - p[0])
.sum::<u64>()
/ (success_measurements.len() - 1) as u64;
let avg_failure_increase = failure_measurements
.windows(2)
.map(|p| p[1] - p[0])
.sum::<u64>()
/ (failure_measurements.len() - 1) as u64;
println!("average success increase: {avg_success_increase}");
println!("average failure increase: {avg_failure_increase}");
assert!(avg_success_increase < 2_050);
assert!(avg_success_increase < 18_500);
Ok(())
}
@ -146,7 +344,7 @@ async fn test_health_compute_tokens_during_maint_weight_shift() -> Result<(), Tr
#[tokio::test]
async fn test_health_compute_serum() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(130_000);
test_builder.test().set_compute_max_units(135_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();

View File

@ -112,8 +112,8 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
taker_fee: 0.0,
group_insurance_fund: true,
// adjust this factur such that we get the desired settle limit in the end
settle_pnl_limit_factor: (settle_limit as f32 + 0.1).min(0.0)
/ (-1.0 * 100.0 * adj_price) as f32,
settle_pnl_limit_factor: (settle_limit as f32 - 0.1).max(0.0)
/ (1.0 * 100.0 * adj_price) as f32,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
},
@ -227,7 +227,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
let account_data = solana.get_account::<MangoAccount>(account).await;
assert_eq!(account_data.perps[0].quote_position_native(), pnl);
assert_eq!(
account_data.perps[0].settle_pnl_limit_realized_trade,
account_data.perps[0].recurring_settle_pnl_allowance,
settle_limit
);
assert_eq!(
@ -277,7 +277,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
};
{
let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await;
let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await;
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
send_tx(
@ -310,7 +310,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
}
{
let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await;
let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await;
fund_insurance(2).await;
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
@ -348,7 +348,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
}
{
let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await;
let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await;
fund_insurance(5).await;
send_tx(
@ -371,7 +371,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
// no insurance
{
let (perp_market, account, liqor) = setup_perp(-28, -50, -10).await;
let (perp_market, account, liqor) = setup_perp(-28, -50, 10).await;
send_tx(
solana,
@ -390,7 +390,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
// no settlement: no settle health
{
let (perp_market, account, liqor) = setup_perp(-200, -50, -10).await;
let (perp_market, account, liqor) = setup_perp(-200, -50, 10).await;
fund_insurance(5).await;
send_tx(
@ -430,7 +430,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
// no socialized loss: fully covered by insurance fund
{
let (perp_market, account, liqor) = setup_perp(-40, -50, -5).await;
let (perp_market, account, liqor) = setup_perp(-40, -50, 5).await;
fund_insurance(42).await;
send_tx(

View File

@ -88,7 +88,8 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
init_base_asset_weight: 0.6,
maint_base_liab_weight: 1.3,
init_base_liab_weight: 1.4,
base_liquidation_fee: 0.05,
base_liquidation_fee: 0.03,
platform_liquidation_fee: 0.02,
maker_fee: 0.0,
taker_fee: 0.0,
group_insurance_fund: true,
@ -196,6 +197,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
//
// TEST: Liquidate base position with limit
//
let perp_market_before = solana.get_account::<PerpMarket>(perp_market).await;
send_tx(
solana,
PerpLiqBaseOrPositivePnlInstruction {
@ -209,29 +211,41 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
)
.await
.unwrap();
let perp_market_after = solana.get_account::<PerpMarket>(perp_market).await;
let liq_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.03);
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(
liqor_data.perps[0].quote_position_native(),
-liq_amount,
-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(
liqee_data.perps[0].quote_position_native(),
-20.0 * 100.0 + liq_amount,
-20.0 * 100.0 + liqee_amount,
0.1
));
assert!(assert_equal(
liqee_data.perps[0].realized_trade_pnl_native,
liq_amount - 1000.0,
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].settle_pnl_limit_realized_trade, -201);
assert_eq!(liqee_data.perps[0].recurring_settle_pnl_allowance, 201);
assert!(assert_equal(
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
liqor_amount - liqee_amount,
0.1,
));
assert!(assert_equal(
perp_market_after.accrued_liquidation_fees - perp_market_before.accrued_liquidation_fees,
liqor_amount - liqee_amount,
0.1,
));
//
// TEST: Liquidate base position max
@ -250,19 +264,20 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_2 = 6.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_amount_2 = 6.0 * 100.0 * 0.6 * (1.0 - 0.03);
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(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2,
-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(
liqee_data.perps[0].quote_position_native(),
-20.0 * 100.0 + liq_amount + liq_amount_2,
-20.0 * 100.0 + liqee_amount + liqee_amount_2,
0.1
));
@ -304,6 +319,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
//
// TEST: Liquidate base position
//
let perp_market_before = solana.get_account::<PerpMarket>(perp_market).await;
send_tx(
solana,
PerpLiqBaseOrPositivePnlInstruction {
@ -317,22 +333,34 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
)
.await
.unwrap();
let perp_market_after = solana.get_account::<PerpMarket>(perp_market).await;
let liq_amount_3 = 10.0 * 100.0 * 1.32 * (1.0 + 0.05);
let liqor_amount_3 = 10.0 * 100.0 * 1.32 * (1.0 + 0.03);
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(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3,
-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(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_3,
20.0 * 100.0 - liqee_amount_3,
0.1
));
assert!(assert_equal(
perp_market_after.fees_accrued - perp_market_before.fees_accrued,
liqee_amount_3 - liqor_amount_3,
0.1,
));
assert!(assert_equal(
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
@ -351,19 +379,20 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_4 = 7.0 * 100.0 * 1.32 * (1.0 + 0.05);
let liqor_amount_4 = 7.0 * 100.0 * 1.32 * (1.0 + 0.03);
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(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4,
-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(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_3 - liq_amount_4,
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4,
0.1
));
@ -405,19 +434,20 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_5 = 3.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liqor_amount_5 = 3.0 * 100.0 * 2.0 * (1.0 + 0.03);
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(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4 + liq_amount_5,
-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(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5,
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4 - liqee_amount_5,
0.1
));
@ -446,7 +476,8 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqee_quote_deposits_before: f64 = 1329.0;
// the liqor's settle limit means we can't settle everything
let settle_amount = liqee_quote_deposits_before.min(liqor_max_settle as f64);
let remaining_pnl = 20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + settle_amount;
let remaining_pnl =
20.0 * 100.0 - liqee_amount_3 - liqee_amount_4 - liqee_amount_5 + settle_amount;
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);
@ -490,7 +521,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
send_tx(
solana,
TokenWithdrawInstruction {
amount: liqee_quote_deposits_before as u64 - 100,
amount: liqee_quote_deposits_before as u64 - 200,
allow_borrow: false,
account: account_1,
owner,
@ -531,7 +562,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
// the amount of perp quote transfered
let liq_perp_quote_amount =
(insurance_vault_funding as f64) / 1.05 + (-liqee_settle_limit_before) as f64;
(insurance_vault_funding as f64) / 1.03 + (-liqee_settle_limit_before) as f64;
// insurance fund was depleted and the liqor received it
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
@ -541,9 +572,9 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
0.1
));
assert!(assert_equal(
liqor_data.tokens[0].native(&settle_bank),
liqor_before.tokens[0].native(&settle_bank).to_num::<f64>()
- liqee_settle_limit_before as f64 * 100.0, // 100 is base lot size
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
));

View File

@ -192,6 +192,25 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
let collateral_token1 = &tokens[2];
let collateral_token2 = &tokens[3];
for token in &tokens[0..4] {
send_tx(
solana,
TokenEdit {
group,
admin,
mint: token.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
liquidation_fee_opt: Some(0.01),
platform_liquidation_fee_opt: Some(0.01),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
}
// deposit some funds, to the vaults aren't empty
let vault_account = send_tx(
solana,
@ -325,12 +344,42 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
.await
.unwrap();
// the we only have 20 collateral2, and can trade them for 20 / 1.02 = 19.6 borrow2
// the liqee's 20 collateral2 are traded for 20 / (1.02 * 1.02) = 19.22 borrow2
// (liq fee is 1% liqor + 1% platform for both sides)
assert_eq!(
account_position(solana, account, borrow_token2.bank).await,
-50 + 20
-50 + 19
);
assert_eq!(
account_position(solana, vault_account, borrow_token2.bank).await,
100000 - 19
);
// All liqee collateral2 is gone
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
// 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(
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
));
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
@ -354,11 +403,11 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
.await
.unwrap();
// the asset cost for 50-19=31 borrow2 is 31 * 1.02 = 31.62
// the asset cost for 50-19=31 borrow2 is 31 * 1.02 * 1.02 = 32.25
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
1000 - 31
1000 - 32
);
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
@ -382,14 +431,14 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
.await
.unwrap();
// the asset cost for 10 borrow1 is 10 * 2 * 1.02 = 20.4
// the asset cost for 10 borrow1 is 10 * 2 * 1.02 * 1.02 = 20.8
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
-350 + 10
);
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
1000 - 31 - 20
1000 - 32 - 21
);
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
@ -414,16 +463,16 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
.unwrap();
// health after borrow2 liquidation was (1000-32) * 0.6 - 350 * 2 * 1.4 = -399.2
// borrow1 needed 399.2 / (1.4*2 - 0.6*2*1.02) = 253.29
// asset cost = 253.29 * 2 * 1.02 = 516.7
// borrow1 needed 399.2 / (1.4*2 - 0.6*2*1.02*1.02) = 257.30
// asset cost = 257.30 * 2 * 1.02 * 1.02 = 535.39
// loan orignation fee = 1
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
-350 + 253
-350 + 257
);
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
1000 - 31 - 516 - 1
1000 - 32 - 535 - 1
);
let liqee = get_mango_account(solana, account).await;
assert!(!liqee.being_liquidated());
@ -433,11 +482,12 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
//
// Setup: make collateral really valueable, remove nearly all of it
set_bank_stub_oracle_price(solana, group, collateral_token1, admin, 100000.0).await;
set_bank_stub_oracle_price(solana, group, collateral_token1, admin, 100_000.0).await;
send_tx(
solana,
TokenWithdrawInstruction {
amount: (account_position(solana, account, collateral_token1.bank).await) as u64 - 1,
// -2 to avoid removing _all_ collateral if account_position() rounded up and dusting happens
amount: (account_position(solana, account, collateral_token1.bank).await) as u64 - 2,
allow_borrow: false,
account,
owner,
@ -447,10 +497,17 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
)
.await
.unwrap();
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
-93
);
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
2
);
// Setup: reduce collateral value to trigger liquidatability
// We have -93 borrows, so -93*2*1.4 = -260.4 health from that
// And 1-2 collateral, so max 2*0.6*X health; say X=150 for max 180 health
set_bank_stub_oracle_price(solana, group, collateral_token1, admin, 150.0).await;
set_bank_stub_oracle_price(solana, group, collateral_token1, admin, 75.0).await;
send_tx(
solana,
@ -471,7 +528,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
// Liqee's remaining collateral got dusted, only borrows remain: the account is bankrupt
let liqee = get_mango_account(solana, account).await;
assert_eq!(liqee.active_token_positions().count(), 1);
assert!(account_position_f64(solana, account, borrow_token1.bank).await > -2.74);
// up from -93
assert!(account_position_f64(solana, account, borrow_token1.bank).await > -26.0);
assert!(liqee.being_liquidated());
Ok(())

View File

@ -244,7 +244,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
#[tokio::test]
async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(105_000);
test_builder.test().set_compute_max_units(150_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
@ -278,6 +278,7 @@ async fn test_flash_loan_swap_fee() -> Result<(), BanksClientError> {
group,
admin,
mint: tokens[1].mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
flash_loan_swap_fee_rate_opt: Some(swap_fee_rate as f32),
..token_edit_instruction_default()
@ -523,16 +524,19 @@ async fn test_flash_loan_creates_ata_accounts() -> Result<(), BanksClientError>
}
//
// SETUP: Verify atas are empty
// SETUP: Wipe owner ATAs that are set up by default
//
let owner_token0_ata = anchor_spl::associated_token::get_associated_token_address(
&owner.pubkey(),
&mints[0].pubkey,
);
let owner_token1_ata = anchor_spl::associated_token::get_associated_token_address(
&owner.pubkey(),
&mints[1].pubkey,
);
use solana_sdk::account::AccountSharedData;
let owner_token0_ata = context.users[0].token_accounts[0];
let owner_token1_ata = context.users[0].token_accounts[1];
solana
.context
.borrow_mut()
.set_account(&owner_token0_ata, &AccountSharedData::default());
solana
.context
.borrow_mut()
.set_account(&owner_token1_ata, &AccountSharedData::default());
assert!(solana.get_account_data(owner_token0_ata).await.is_none());
assert!(solana.get_account_data(owner_token1_ata).await.is_none());
@ -650,6 +654,7 @@ async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
group,
admin,
mint: tokens[0].mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
deposit_limit_opt: Some(1000),
..token_edit_instruction_default()

View File

@ -1439,6 +1439,160 @@ async fn test_perp_compute() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_perp_cancel_with_in_flight_events() -> 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_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&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));
//
// SETUP: Place a bid, a matching ask, generating a closing fill event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 2,
client_order_id: 6,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
//
// TEST: it's possible to cancel, freeing up the user's oo slot
//
send_tx(
solana,
PerpCancelAllOrdersInstruction {
account: account_0,
perp_market,
owner,
limit: 10,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.bids_base_lots, 2);
assert!(!mango_account_0.perp_open_orders[0].is_active());
//
// TEST: consuming the event updates the perp account state
//
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.bids_base_lots, 0);
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

@ -975,6 +975,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
send_tx(
solana,
StubOracleSetInstruction {
oracle: tokens[1].oracle,
group,
admin,
mint: mints[1].pubkey,
@ -1099,14 +1100,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].realized_trade_pnl_native,
I80F48::from(200_000 - 80_000)
);
assert_eq!(
mango_account_1.perps[0].realized_trade_pnl_native,
I80F48::from(-200_000 + 80_000)
);
// neither account has any settle limit left (check for 1 because of the ceil()ing)
assert_eq!(
mango_account_0.perps[0].available_settle_limit(&market).1,
@ -1118,7 +1111,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
);
// check that realized pnl settle limit was set up correctly
assert_eq!(
mango_account_0.perps[0].settle_pnl_limit_realized_trade,
mango_account_0.perps[0].recurring_settle_pnl_allowance,
(0.8 * 1.0 * 100.0 * 1000.0) as i64 + 1
); // +1 just for rounding
@ -1151,7 +1144,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
// This time account 0's realized pnl settle limit kicks in.
//
let account_1_quote_before = mango_account_1.perps[0].quote_position_native();
let account_0_realized_limit = mango_account_0.perps[0].settle_pnl_limit_realized_trade;
let account_0_realized_limit = mango_account_0.perps[0].recurring_settle_pnl_allowance;
send_tx(
solana,
@ -1185,12 +1178,13 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
mango_account_1.perps[0].quote_position_native() - account_1_quote_before,
I80F48::from(account_0_realized_limit)
);
// account0's limit gets reduced to the realized pnl amount left over
// account0's limit gets reduced to the pnl amount left over
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
mango_account_0.perps[0].settle_pnl_limit_realized_trade,
mango_account_0.perps[0].recurring_settle_pnl_allowance,
mango_account_0.perps[0]
.realized_trade_pnl_native
.to_num::<i64>()
.unsettled_pnl(&perp_market_data, I80F48::from_num(1.0))
.unwrap()
);
// can't settle again
@ -1212,7 +1206,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
//
let account_1_quote_before = mango_account_1.perps[0].quote_position_native();
let account_0_realized_limit = mango_account_0.perps[0].settle_pnl_limit_realized_trade;
let account_0_realized_limit = mango_account_0.perps[0].recurring_settle_pnl_allowance;
send_tx(
solana,
@ -1247,13 +1241,13 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
I80F48::from(account_0_realized_limit)
);
// account0's limit gets reduced to the realized pnl amount left over
assert_eq!(mango_account_0.perps[0].settle_pnl_limit_realized_trade, 0);
assert_eq!(mango_account_0.perps[0].recurring_settle_pnl_allowance, 0);
assert_eq!(
mango_account_0.perps[0].realized_trade_pnl_native,
mango_account_0.perps[0].realized_pnl_for_position_native,
I80F48::from(0)
);
assert_eq!(
mango_account_1.perps[0].realized_trade_pnl_native,
mango_account_1.perps[0].realized_pnl_for_position_native,
I80F48::from(0)
);

View File

@ -1,6 +1,7 @@
#![allow(dead_code)]
use super::*;
use anchor_lang::prelude::AccountMeta;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::serum3_cpi::{load_open_orders_bytes, OpenOrdersSlim};
use std::sync::Arc;
@ -134,6 +135,20 @@ impl SerumOrderPlacer {
.unwrap();
}
async fn cancel_by_client_order_id(&self, client_order_id: u64) {
send_tx(
&self.solana,
Serum3CancelOrderByClientOrderIdInstruction {
client_order_id,
account: self.account,
owner: self.owner,
serum_market: self.serum_market,
},
)
.await
.unwrap();
}
async fn cancel_all(&self) {
let open_orders = self.serum.load_open_orders(self.open_orders).await;
let orders = open_orders.orders;
@ -357,6 +372,14 @@ async fn test_serum_basics() -> Result<(), TransportError> {
//
order_placer.cancel(order_id).await;
//
// TEST: Cancel order by client order id
//
let (_, _) = order_placer.bid_maker(1.0, 100).await.unwrap();
order_placer
.cancel_by_client_order_id(order_placer.next_client_order_id - 1)
.await;
//
// TEST: Settle, moving the freed up funds back
//
@ -1475,6 +1498,182 @@ async fn test_serum_compute() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_fallback_oracle_serum() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(150_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let fallback_oracle_kp = TestKeypair::new();
let fallback_oracle = fallback_oracle_kp.pubkey();
let owner = context.users[0].key;
let payer = context.users[1].key;
let payer_token_accounts = &context.users[1].token_accounts[0..3];
//
// SETUP: Create a group and an account
//
let deposit_amount = 1_000;
let CommonSetup {
group_with_tokens,
quote_token,
base_token,
mut order_placer,
..
} = common_setup(&context, deposit_amount).await;
let GroupWithTokens {
group,
admin,
tokens,
..
} = group_with_tokens;
//
// SETUP: Create a fallback oracle
//
send_tx(
solana,
StubOracleCreate {
oracle: fallback_oracle_kp,
group,
mint: tokens[2].mint.pubkey,
admin,
payer,
},
)
.await
.unwrap();
//
// SETUP: Add a fallback oracle
//
send_tx(
solana,
TokenEdit {
group,
admin,
mint: tokens[2].mint.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);
// Create some token1 borrows
send_tx(
solana,
TokenWithdrawInstruction {
amount: 1_500,
allow_borrow: true,
account: order_placer.account,
owner,
token_account: payer_token_accounts[2],
bank_index: 0,
},
)
.await
.unwrap();
// Make oracle invalid by increasing deviation
send_tx(
solana,
StubOracleSetTestInstruction {
oracle: tokens[2].oracle,
group,
mint: tokens[2].mint.pubkey,
admin,
price: 1.0,
last_update_slot: 0,
deviation: 100.0,
},
)
.await
.unwrap();
//
// TEST: Place a failing order
//
let limit_price = 1.0;
let max_base = 100;
let order_fut = order_placer.try_bid(limit_price, max_base, false).await;
assert_mango_error(
&order_fut,
6023,
"an oracle does not reach the confidence threshold".to_string(),
);
// now send txn with a fallback oracle in the remaining accounts
let fallback_oracle_meta = AccountMeta {
pubkey: fallback_oracle,
is_writable: false,
is_signer: false,
};
let client_order_id = order_placer.inc_client_order_id();
let place_ix = 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)).ceil()
as u64,
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
order_type: Serum3OrderType::Limit,
client_order_id,
limit: 10,
account: order_placer.account,
owner: order_placer.owner,
serum_market: order_placer.serum_market,
};
let result = send_tx_with_extra_accounts(solana, place_ix, vec![fallback_oracle_meta])
.await
.unwrap();
result.result.unwrap();
let account_data = get_mango_account(solana, order_placer.account).await;
assert_eq!(
account_data
.token_position_by_raw_index(0)
.unwrap()
.in_use_count,
1
);
assert_eq!(
account_data
.token_position_by_raw_index(1)
.unwrap()
.in_use_count,
1
);
assert_eq!(
account_data
.token_position_by_raw_index(2)
.unwrap()
.in_use_count,
0
);
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
assert_eq!(serum_orders.base_borrows_without_fee, 0);
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
assert_eq!(serum_orders.potential_base_tokens, 100);
assert_eq!(serum_orders.potential_quote_tokens, 100);
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
assert_eq!(base_bank.potential_serum_tokens, 100);
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
assert_eq!(quote_bank.potential_serum_tokens, 100);
Ok(())
}
#[tokio::test]
async fn test_serum_bands() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
@ -1563,6 +1762,7 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
//
let deposit_amount = 5000; // for 10k tokens over both order_placers
let CommonSetup {
serum_market_cookie,
group_with_tokens,
mut order_placer,
quote_token,
@ -1599,6 +1799,7 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
group: group_with_tokens.group,
admin: group_with_tokens.admin,
mint: base_token.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
deposit_limit_opt: Some(13000),
..token_edit_instruction_default()
@ -1649,6 +1850,7 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
group: group_with_tokens.group,
admin: group_with_tokens.admin,
mint: base_token.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
deposit_limit_opt: Some(0),
..token_edit_instruction_default()
@ -1663,6 +1865,7 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
group: group_with_tokens.group,
admin: group_with_tokens.admin,
mint: quote_token.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
deposit_limit_opt: Some(13000),
..token_edit_instruction_default()
@ -1677,11 +1880,15 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
let remaining_quote = {
|| async {
let b: Bank = solana2.get_account(quote_bank).await;
b.remaining_deposits_until_limit().round().to_num::<u64>()
b.remaining_deposits_until_limit().round().to_num::<i64>()
}
};
order_placer.cancel_all().await;
context
.serum
.consume_spot_events(&serum_market_cookie, &[order_placer.open_orders])
.await;
//
// TEST: even when placing all quote tokens into a bid, they still count
@ -1705,6 +1912,43 @@ async fn test_serum_deposit_limits() -> Result<(), TransportError> {
assert_mango_error(&r, MangoError::BankDepositLimit.into(), "dep limit".into());
order_placer.try_ask(5.0, 399).await.unwrap(); // not 400 due to rounding
// reset
order_placer.cancel_all().await;
context
.serum
.consume_spot_events(&serum_market_cookie, &[order_placer.open_orders])
.await;
order_placer.settle().await;
//
// TEST: can place a bid even if quote deposit limit is exhausted
//
send_tx(
solana,
TokenEdit {
group: group_with_tokens.group,
admin: group_with_tokens.admin,
mint: quote_token.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
deposit_limit_opt: Some(1),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
assert!(remaining_quote().await < 0);
assert_eq!(
account_position(solana, order_placer.account, quote_token.bank).await,
5000
);
// borrowing might lead to a deposit increase later
let r = order_placer.try_bid(1.0, 5001, false).await;
assert_mango_error(&r, MangoError::BankDepositLimit.into(), "dep limit".into());
// but just selling deposits is fine
order_placer.try_bid(1.0, 4999, false).await.unwrap();
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More