Merge tag 'program-v0.22.0' into deploy
This commit is contained in:
commit
27ecc14000
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}'
|
||||
|
|
43
CHANGELOG.md
43
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -3367,7 +3367,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
version = "0.21.2"
|
||||
version = "0.22.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(®istry);
|
||||
|
|
|
@ -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 {
|
||||
(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(®istry);
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
646
mango_v4.json
646
mango_v4.json
|
@ -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)"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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>(),
|
||||
)]
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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),
|
||||
)?;
|
||||
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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![];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
|
@ -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>(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
));
|
||||
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue