diff --git a/Cargo.lock b/Cargo.lock index 44fd12c18..682b1f477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,20 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f6ee9518f50ff4d434471ccf569186022bdd5ef65a21d14da3ea5231af944f" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "proc-macro2 1.0.39", + "quote 1.0.18", + "regex", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "proc-macro2 1.0.39", "quote 1.0.18", @@ -121,7 +134,21 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32c92bcf5388b52676d990f85bbfd838a8f5672393135063a50dc79b2b837c79" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "bs58 0.4.0", + "proc-macro2 1.0.39", + "quote 1.0.18", + "rustversion", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "bs58 0.4.0", "proc-macro2 1.0.39", @@ -136,7 +163,17 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0844974ac35e8ced62056b0d63777ebcdc5807438b8b189c881e2b647450b70a" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.39", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "proc-macro2 1.0.39", "syn 1.0.95", ] @@ -147,7 +184,18 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f7467345e67a6f1d4b862b9763a4160ad89d18c247b8c902807768f7b6e23df" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "proc-macro2 1.0.39", "quote 1.0.18", "syn 1.0.95", @@ -159,7 +207,19 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8774e4c1ac71f71a5aea7e4932fb69c30e3b8155c4fa59fd69401195434528a9" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "proc-macro2 1.0.39", "quote 1.0.18", @@ -172,7 +232,20 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90eeb6e1c80f9f94fcef93a52813f6472186200e275e83cb3fac92b801de92f7" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "heck 0.3.3", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-interface" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "heck 0.3.3", "proc-macro2 1.0.39", @@ -186,7 +259,19 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac515a7a5a4fea7fc768b1cec40ddb948e148ea657637c75f94f283212326cb9" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "proc-macro2 1.0.39", "quote 1.0.18", @@ -199,7 +284,19 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43dc667b62ff71450f19dcfcc37b0c408fd4ddd89e8650368c2b0984b110603f" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "anchor-attribute-state" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "proc-macro2 1.0.39", "quote 1.0.18", @@ -209,10 +306,9 @@ dependencies = [ [[package]] name = "anchor-client" version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee0e630f9310a0134c92df4458890a0f9c5b662d69c305690af1c17f5cd0b3ba" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" dependencies = [ - "anchor-lang", + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "regex", "serde", @@ -229,7 +325,19 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7354d583a06701d24800a8ec4c2b0491f62581a331af349205e23421e0b56643" dependencies = [ - "anchor-syn", + "anchor-syn 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "proc-macro2 1.0.39", + "quote 1.0.18", + "syn 1.0.95", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-syn 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "proc-macro2 1.0.39", "quote 1.0.18", @@ -242,15 +350,38 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff5f57ec5e12fa6874b27f3d5c1f6f44302d3ad86c1266197ff7611bf6f5d251" dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-constant", - "anchor-attribute-error", - "anchor-attribute-event", - "anchor-attribute-interface", - "anchor-attribute-program", - "anchor-attribute-state", - "anchor-derive-accounts", + "anchor-attribute-access-control 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-account 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-constant 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-error 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-event 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-interface 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-program 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-attribute-state 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-derive-accounts 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "arrayref", + "base64 0.13.0", + "bincode", + "borsh", + "bytemuck", + "solana-program", + "thiserror", +] + +[[package]] +name = "anchor-lang" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-attribute-access-control 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-account 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-constant 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-error 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-event 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-interface 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-program 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-attribute-state 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-derive-accounts 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "arrayref", "base64 0.13.0", "bincode", @@ -266,7 +397,18 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65904c3106851f6d1bb87d504044764819d69c51d2b4346d59d399d8afa7d18" dependencies = [ - "anchor-lang", + "anchor-lang 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-program", + "spl-associated-token-account", + "spl-token", +] + +[[package]] +name = "anchor-spl" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "solana-program", "spl-associated-token-account", "spl-token", @@ -291,6 +433,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "anchor-syn" +version = "0.25.0" +source = "git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab#1153380487706e4d8f486071cf2f519468438eab" +dependencies = [ + "anyhow", + "bs58 0.3.1", + "heck 0.3.3", + "proc-macro2 1.0.39", + "proc-macro2-diagnostics", + "quote 1.0.18", + "serde", + "serde_json", + "sha2 0.9.9", + "syn 1.0.95", + "thiserror", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -1039,8 +1199,8 @@ name = "client" version = "0.1.0" dependencies = [ "anchor-client", - "anchor-lang", - "anchor-spl", + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-spl 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "fixed", "fixed-macro", @@ -1048,6 +1208,7 @@ dependencies = [ "mango-v4", "pyth-sdk-solana", "serum_dex", + "solana-account-decoder", "solana-client", "solana-sdk", "thiserror", @@ -2662,8 +2823,8 @@ name = "keeper" version = "0.1.0" dependencies = [ "anchor-client", - "anchor-lang", - "anchor-spl", + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-spl 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "clap 3.1.18", "client", @@ -2816,7 +2977,7 @@ name = "liquidator" version = "0.0.1" dependencies = [ "anchor-client", - "anchor-lang", + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "anyhow", "arrayref", "async-channel", @@ -2935,8 +3096,8 @@ dependencies = [ name = "mango-v4" version = "0.1.0" dependencies = [ - "anchor-lang", - "anchor-spl", + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-spl 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "arrayref", "async-trait", "base64 0.13.0", @@ -2972,8 +3133,8 @@ dependencies = [ name = "margin-trade" version = "0.1.0" dependencies = [ - "anchor-lang", - "anchor-spl", + "anchor-lang 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", + "anchor-spl 0.25.0 (git+https://github.com/blockworks-foundation/anchor.git?rev=1153380487706e4d8f486071cf2f519468438eab)", "solana-program", ] @@ -6352,8 +6513,8 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e01007eb005d7b5d4b6daaec7db6fe0b5ce9a0c6cb28b00d5c94dc7bfacc075" dependencies = [ - "anchor-lang", - "anchor-spl", + "anchor-lang 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anchor-spl 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytemuck", "rust_decimal", "solana-program", diff --git a/Cargo.toml b/Cargo.toml index f09a70deb..371aad536 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ members = [ [patch.crates-io] # for gzip encoded responses -jsonrpc-core-client = { git = "https://github.com/ckamm/jsonrpc.git", branch = "ckamm/http-with-gzip" } \ No newline at end of file +jsonrpc-core-client = { git = "https://github.com/ckamm/jsonrpc.git", branch = "ckamm/http-with-gzip" } diff --git a/client/Cargo.toml b/client/Cargo.toml index 48a8d7bd5..625006684 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" doctest = false [dependencies] -anchor-client = "0.25.0" -anchor-lang = "0.25.0" -anchor-spl = "0.25.0" +anchor-client = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-lang = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-spl = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } anyhow = "1.0" fixed = { version = "=1.11.0", features = ["serde", "borsh"] } fixed-macro = "^1.1.1" @@ -17,6 +17,7 @@ itertools = "0.10.3" mango-v4 = { path = "../programs/mango-v4" } pyth-sdk-solana = "0.1.0" serum_dex = { version = "0.4.0", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false, features = ["no-entrypoint", "program"] } +solana-account-decoder = "~1.10.29" solana-client = "~1.10.29" solana-sdk = "~1.10.29" thiserror = "1.0.31" diff --git a/client/src/account_fetcher.rs b/client/src/account_fetcher.rs index 11be7b441..88bec46b6 100644 --- a/client/src/account_fetcher.rs +++ b/client/src/account_fetcher.rs @@ -1,16 +1,17 @@ use std::collections::HashMap; use std::sync::Mutex; -use anchor_client::ClientError; +use anyhow::Context; +use anchor_client::ClientError; use anchor_lang::AccountDeserialize; use solana_client::rpc_client::RpcClient; - -use anyhow::Context; use solana_sdk::account::Account; use solana_sdk::pubkey::Pubkey; +use mango_v4::state::MangoAccountValue; + pub trait AccountFetcher: Sync + Send { fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result; } @@ -22,7 +23,19 @@ pub fn account_fetcher_fetch_anchor_account( ) -> anyhow::Result { let account = fetcher.fetch_raw_account(address)?; let mut data: &[u8] = &account.data; - T::try_deserialize(&mut data).with_context(|| format!("deserializing account {}", address)) + T::try_deserialize(&mut data) + .with_context(|| format!("deserializing anchor account {}", address)) +} + +// Can't be in the trait, since then it would no longer be object-safe... +pub fn account_fetcher_fetch_mango_account( + fetcher: &dyn AccountFetcher, + address: Pubkey, +) -> anyhow::Result { + let account = fetcher.fetch_raw_account(address)?; + let data: &[u8] = &account.data; + MangoAccountValue::from_bytes(&data[8..]) + .with_context(|| format!("deserializing mango account {}", address)) } pub struct RpcAccountFetcher { diff --git a/client/src/client.rs b/client/src/client.rs index 6942c57ea..aab1163bc 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -11,7 +11,7 @@ use anchor_spl::token::Token; use fixed::types::I80F48; use itertools::Itertools; use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; -use mango_v4::state::{Bank, Group, MangoAccount, Serum3MarketIndex, TokenIndex}; +use mango_v4::state::{AccountSize, Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex}; use solana_client::rpc_client::RpcClient; @@ -100,12 +100,16 @@ impl MangoClient { let mut mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?; let mango_account_opt = mango_account_tuples .iter() - .find(|tuple| tuple.1.name() == mango_account_name); + .find(|(_, account)| account.fixed.name() == mango_account_name); if mango_account_opt.is_none() { - mango_account_tuples - .sort_by(|a, b| a.1.account_num.partial_cmp(&b.1.account_num).unwrap()); + mango_account_tuples.sort_by(|a, b| { + a.1.fixed + .account_num + .partial_cmp(&b.1.fixed.account_num) + .unwrap() + }); let account_num = match mango_account_tuples.last() { - Some(tuple) => tuple.1.account_num + 1, + Some(tuple) => tuple.1.fixed.account_num + 1, None => 0u8, }; program @@ -137,6 +141,7 @@ impl MangoClient { &mango_v4::instruction::AccountCreate { account_num, name: mango_account_name.to_owned(), + account_size: AccountSize::Small, }, ), }) @@ -146,9 +151,9 @@ impl MangoClient { let mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?; let index = mango_account_tuples .iter() - .position(|tuple| tuple.1.name() == mango_account_name) + .position(|tuple| tuple.1.fixed.name() == mango_account_name) .unwrap(); - let mango_account_cache = mango_account_tuples[index]; + let mango_account_cache = &mango_account_tuples[index]; Ok(Self { rpc, @@ -181,8 +186,8 @@ impl MangoClient { self.context.group } - pub fn mango_account(&self) -> anyhow::Result { - account_fetcher_fetch_anchor_account(&*self.account_fetcher, self.mango_account_address) + pub fn mango_account(&self) -> anyhow::Result { + account_fetcher_fetch_mango_account(&*self.account_fetcher, self.mango_account_address) } pub fn first_bank(&self, token_index: TokenIndex) -> anyhow::Result { @@ -205,7 +210,7 @@ impl MangoClient { pub fn derive_liquidation_health_check_remaining_account_metas( &self, - liqee: &MangoAccount, + liqee: &MangoAccountValue, asset_token_index: TokenIndex, liab_token_index: TokenIndex, ) -> anyhow::Result> { @@ -215,9 +220,8 @@ impl MangoClient { let account = self.mango_account()?; let token_indexes = liqee - .tokens - .iter_active() - .chain(account.tokens.iter_active()) + .token_iter_active() + .chain(account.token_iter_active()) .map(|ta| ta.token_index) .unique(); @@ -229,14 +233,12 @@ impl MangoClient { } let serum_oos = liqee - .serum3 - .iter_active() - .chain(account.serum3.iter_active()) + .serum3_iter_active() + .chain(account.serum3_iter_active()) .map(|&s| s.open_orders); let perp_markets = liqee - .perps - .iter_active_accounts() - .chain(account.perps.iter_active_accounts()) + .perp_iter_active_accounts() + .chain(account.perp_iter_active_accounts()) .map(|&pa| self.context.perp_market_address(pa.market_index)); let to_account_meta = |pubkey| AccountMeta { @@ -392,7 +394,7 @@ impl MangoClient { let s3 = self.serum3_data(name)?; let account = self.mango_account()?; - let open_orders = account.serum3.find(s3.market_index).unwrap().open_orders; + let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders; let health_check_metas = self.derive_health_check_remaining_account_metas(None, false)?; @@ -506,7 +508,7 @@ impl MangoClient { let s3 = self.serum3_data(name)?; let account = self.mango_account()?; - let open_orders = account.serum3.find(s3.market_index).unwrap().open_orders; + let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders; self.program() .request() @@ -547,7 +549,7 @@ impl MangoClient { .get(market_name) .unwrap(); let account = self.mango_account()?; - let open_orders = account.serum3.find(market_index).unwrap().open_orders; + let open_orders = account.serum3_find(market_index).unwrap().open_orders; let open_orders_bytes = self.account_fetcher.fetch_raw_account(open_orders)?.data; let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes( @@ -579,7 +581,7 @@ impl MangoClient { let s3 = self.serum3_data(market_name)?; let account = self.mango_account()?; - let open_orders = account.serum3.find(s3.market_index).unwrap().open_orders; + let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders; self.program() .request() @@ -622,7 +624,7 @@ impl MangoClient { pub fn liq_token_with_token( &self, - liqee: (&Pubkey, &MangoAccount), + liqee: (&Pubkey, &MangoAccountValue), asset_token_index: TokenIndex, liab_token_index: TokenIndex, max_liab_transfer: I80F48, @@ -666,7 +668,7 @@ impl MangoClient { pub fn liq_token_bankruptcy( &self, - liqee: (&Pubkey, &MangoAccount), + liqee: (&Pubkey, &MangoAccountValue), liab_token_index: TokenIndex, max_liab_transfer: I80F48, ) -> anyhow::Result { diff --git a/client/src/context.rs b/client/src/context.rs index 3fc417cbb..5e5f0dc43 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -5,7 +5,7 @@ use anchor_client::{Client, ClientError, Cluster, Program}; use anchor_lang::__private::bytemuck; use mango_v4::state::{ - MangoAccount, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex, + MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex, TokenIndex, }; @@ -186,43 +186,36 @@ impl MangoGroupContext { pub fn derive_health_check_remaining_account_metas( &self, - account: &MangoAccount, + account: &MangoAccountValue, affected_token: Option, writable_banks: bool, ) -> anyhow::Result> { // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; let mut oracles = vec![]; - for position in account.tokens.iter_active() { + for position in account.token_iter_active() { let mint_info = self.mint_info(position.token_index); banks.push(mint_info.first_bank()); oracles.push(mint_info.oracle); } if let Some(affected_token_index) = affected_token { if account - .tokens - .iter_active() + .token_iter_active() .find(|p| p.token_index == affected_token_index) .is_none() { // If there is not yet an active position for the token, we need to pass // the bank/oracle for health check anyway. - let new_position = account - .tokens - .values - .iter() - .position(|p| !p.is_active()) - .unwrap(); + let new_position = account.token_iter().position(|p| !p.is_active()).unwrap(); let mint_info = self.mint_info(affected_token_index); banks.insert(new_position, mint_info.first_bank()); oracles.insert(new_position, mint_info.oracle); } } - let serum_oos = account.serum3.iter_active().map(|&s| s.open_orders); + let serum_oos = account.serum3_iter_active().map(|&s| s.open_orders); let perp_markets = account - .perps - .iter_active_accounts() + .perp_iter_active_accounts() .map(|&pa| self.perp_market_address(pa.market_index)); Ok(banks diff --git a/client/src/gpa.rs b/client/src/gpa.rs index 1f0b7d2bd..5acc5b4b9 100644 --- a/client/src/gpa.rs +++ b/client/src/gpa.rs @@ -1,7 +1,10 @@ use anchor_client::{ClientError, Program}; +use anchor_lang::Discriminator; -use mango_v4::state::{Bank, MangoAccount, MintInfo, PerpMarket, Serum3Market}; +use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market}; +use solana_account_decoder::UiAccountEncoding; +use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}; use solana_sdk::pubkey::Pubkey; @@ -9,19 +12,37 @@ pub fn fetch_mango_accounts( program: &Program, group: Pubkey, owner: Pubkey, -) -> Result, ClientError> { - program.accounts::(vec![ - RpcFilterType::Memcmp(Memcmp { - offset: 8, - bytes: MemcmpEncodedBytes::Base58(group.to_string()), - encoding: None, - }), - RpcFilterType::Memcmp(Memcmp { - offset: 40, - bytes: MemcmpEncodedBytes::Base58(owner.to_string()), - encoding: None, - }), - ]) +) -> Result, ClientError> { + let config = RpcProgramAccountsConfig { + filters: Some(vec![ + RpcFilterType::Memcmp(Memcmp { + offset: 0, + bytes: MemcmpEncodedBytes::Bytes(MangoAccount::discriminator().to_vec()), + encoding: None, + }), + RpcFilterType::Memcmp(Memcmp { + offset: 8, + bytes: MemcmpEncodedBytes::Base58(group.to_string()), + encoding: None, + }), + RpcFilterType::Memcmp(Memcmp { + offset: 40, + bytes: MemcmpEncodedBytes::Base58(owner.to_string()), + encoding: None, + }), + ]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..RpcAccountInfoConfig::default() + }, + ..RpcProgramAccountsConfig::default() + }; + program + .rpc() + .get_program_accounts_with_config(&program.id(), config)? + .into_iter() + .map(|(key, account)| Ok((key, MangoAccountValue::from_bytes(&account.data[8..])?))) + .collect::, _>>() } pub fn fetch_banks(program: &Program, group: Pubkey) -> Result, ClientError> { diff --git a/keeper/Cargo.toml b/keeper/Cargo.toml index b65f9d5e9..fc79ceaa9 100644 --- a/keeper/Cargo.toml +++ b/keeper/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anchor-client = "0.25.0" -anchor-lang = "0.25.0" -anchor-spl = "0.25.0" +anchor-client = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-lang = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-spl = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } anyhow = "1.0" clap = { version = "3.1.8", features = ["derive", "env"] } client = { path = "../client" } diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs index 1d9aa369d..320e66185 100644 --- a/keeper/src/taker.rs +++ b/keeper/src/taker.rs @@ -77,7 +77,7 @@ fn ensure_oo(mango_client: &Arc) -> Result<(), anyhow::Error> { let account = mango_client.mango_account()?; for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() { - if account.serum3.find(*market_index).is_none() { + if account.serum3_find(*market_index).is_none() { mango_client.serum3_create_open_orders(serum3_market.market.name())?; } } @@ -92,7 +92,7 @@ fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::Error> let bank = mango_client.first_bank(token_index)?; let desired_balance = I80F48::from_num(10_000 * 10u64.pow(bank.mint_decimals as u32)); - let token_account_opt = mango_account.tokens.find(token_index); + let token_account_opt = mango_account.token_find(token_index); let deposit_native = match token_account_opt { Some(token_account) => { diff --git a/liquidator/Cargo.toml b/liquidator/Cargo.toml index e2e05ae3b..934c500ae 100644 --- a/liquidator/Cargo.toml +++ b/liquidator/Cargo.toml @@ -4,8 +4,8 @@ version = "0.0.1" edition = "2021" [dependencies] -anchor-client = "0.25.0" -anchor-lang = "0.25.0" +anchor-lang = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-client = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } anyhow = "1.0" arrayref = "0.3.6" async-channel = "1.6" diff --git a/liquidator/src/chain_data_fetcher.rs b/liquidator/src/chain_data_fetcher.rs index cff8d9f4a..bce92e90f 100644 --- a/liquidator/src/chain_data_fetcher.rs +++ b/liquidator/src/chain_data_fetcher.rs @@ -4,11 +4,12 @@ use crate::chain_data::*; use client::AccountFetcher; use mango_v4::accounts_zerocopy::LoadZeroCopy; +use mango_v4::state::MangoAccountValue; use anyhow::Context; use solana_client::rpc_client::RpcClient; -use solana_sdk::account::AccountSharedData; +use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::pubkey::Pubkey; pub struct ChainDataAccountFetcher { @@ -29,6 +30,12 @@ impl ChainDataAccountFetcher { .clone()) } + pub fn fetch_mango_account(&self, address: &Pubkey) -> anyhow::Result { + let acc = self.fetch_raw(address)?; + Ok(MangoAccountValue::from_bytes(acc.data()) + .with_context(|| format!("loading mango account {}", address))?) + } + // fetches via RPC, stores in ChainData, returns new version pub fn fetch_fresh( &self, @@ -38,6 +45,11 @@ impl ChainDataAccountFetcher { self.fetch(address) } + pub fn fetch_fresh_mango_account(&self, address: &Pubkey) -> anyhow::Result { + self.refresh_account_via_rpc(address)?; + self.fetch_mango_account(address) + } + pub fn fetch_raw(&self, address: &Pubkey) -> anyhow::Result { let chain_data = self.chain_data.read().unwrap(); Ok(chain_data diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 9730cc87a..1c0d73d5f 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -4,7 +4,7 @@ use crate::ChainDataAccountFetcher; use client::{AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; use mango_v4::state::{ new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, - MangoAccount, TokenIndex, + MangoAccountValue, TokenIndex, }; use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; @@ -12,10 +12,10 @@ use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; pub fn new_health_cache_( context: &MangoGroupContext, account_fetcher: &ChainDataAccountFetcher, - account: &MangoAccount, + account: &MangoAccountValue, ) -> anyhow::Result { - let active_token_len = account.tokens.iter_active().count(); - let active_perp_len = account.perps.iter_active_accounts().count(); + let active_token_len = account.token_iter_active().count(); + let active_perp_len = account.perp_iter_active_accounts().count(); let metas = context.derive_health_check_remaining_account_metas(account, None, false)?; let accounts = metas @@ -34,7 +34,7 @@ pub fn new_health_cache_( begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len, }; - new_health_cache(account, &retriever).context("make health cache") + new_health_cache(&account.borrow(), &retriever).context("make health cache") } #[allow(clippy::too_many_arguments)] @@ -47,7 +47,7 @@ pub fn process_account( let min_health_ratio = I80F48::from_num(50.0f64); let quote_token_index = 0; - let account = account_fetcher.fetch::(pubkey)?; + let account = account_fetcher.fetch_mango_account(pubkey)?; let maint_health = new_health_cache_(&mango_client.context, account_fetcher, &account) .expect("always ok") .health(HealthType::Maint); @@ -59,7 +59,7 @@ pub fn process_account( log::trace!( "possible candidate: {}, with owner: {}, maint health: {}, bankrupt: {}", pubkey, - account.owner, + account.fixed.owner, maint_health, account.is_bankrupt(), ); @@ -67,15 +67,14 @@ pub fn process_account( // Fetch a fresh account and re-compute // This is -- unfortunately -- needed because the websocket streams seem to not // be great at providing timely updates to the account data. - let account = account_fetcher.fetch_fresh::(pubkey)?; + let account = account_fetcher.fetch_fresh_mango_account(pubkey)?; let maint_health = new_health_cache_(&mango_client.context, account_fetcher, &account) .expect("always ok") .health(HealthType::Maint); // find asset and liab tokens let mut tokens = account - .tokens - .iter_active() + .token_iter_active() .map(|token_position| { let token = mango_client.context.token(token_position.token_index); let bank = account_fetcher.fetch::(&token.mint_info.first_bank())?; @@ -96,14 +95,14 @@ pub fn process_account( let get_max_liab_transfer = |source, target| -> anyhow::Result { let mut liqor = account_fetcher - .fetch_fresh::(&mango_client.mango_account_address) + .fetch_fresh_mango_account(&mango_client.mango_account_address) .context("getting liquidator account")? .clone(); // Ensure the tokens are activated, so they appear in the health cache and // max_swap_source() will work. - liqor.tokens.get_mut_or_create(source)?; - liqor.tokens.get_mut_or_create(target)?; + liqor.token_get_mut_or_create(source)?; + liqor.token_get_mut_or_create(target)?; let health_cache = new_health_cache_(&mango_client.context, account_fetcher, &liqor).expect("always ok"); diff --git a/liquidator/src/snapshot_source.rs b/liquidator/src/snapshot_source.rs index 13da53b09..ef8a8ac8d 100644 --- a/liquidator/src/snapshot_source.rs +++ b/liquidator/src/snapshot_source.rs @@ -161,9 +161,9 @@ async fn feed_snapshots( }) .flat_map(|mango_account| { mango_account - .serum3 - .iter_active() + .serum3_iter_active() .map(|serum3account| serum3account.open_orders) + .collect::>() }) .collect::>(); diff --git a/liquidator/src/util.rs b/liquidator/src/util.rs index 31b532af3..84f64bffa 100644 --- a/liquidator/src/util.rs +++ b/liquidator/src/util.rs @@ -1,7 +1,7 @@ use anchor_lang::Discriminator; use arrayref::array_ref; -use mango_v4::state::{Bank, MangoAccount, MintInfo, PerpMarket}; +use mango_v4::state::{Bank, MangoAccount, MangoAccountRefWithHeader, MintInfo, PerpMarket}; use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::pubkey::Pubkey; @@ -10,7 +10,7 @@ pub fn is_mango_account<'a>( account: &'a AccountSharedData, program_id: &Pubkey, group_id: &Pubkey, -) -> Option<&'a MangoAccount> { +) -> Option> { let data = account.data(); if account.owner() != program_id || data.is_empty() { return None; @@ -20,11 +20,9 @@ pub fn is_mango_account<'a>( if disc_bytes != &MangoAccount::discriminator() { return None; } - if data.len() != 8 + std::mem::size_of::() { - return None; - } - let mango_account: &MangoAccount = bytemuck::try_from_bytes(&data[8..]).expect("always Ok"); - if mango_account.group != *group_id { + + let mango_account = MangoAccountRefWithHeader::from_bytes(&data[8..]).expect("always ok"); + if mango_account.fixed.group != *group_id { return None; } Some(mango_account) diff --git a/mc-release-to-devnet.sh b/mc-release-to-devnet.sh index 3efa1efc3..4d0f7992f 100755 --- a/mc-release-to-devnet.sh +++ b/mc-release-to-devnet.sh @@ -2,6 +2,13 @@ set -e pipefail +ANCHOR_BRANCH=v0.25.0-mangov4 +ANCHOR_FORK=$(cd ../anchor && git rev-parse --abbrev-ref HEAD) +if [ "$ANCHOR_FORK" != "$ANCHOR_BRANCH" ]; then + echo "Check out anchor fork at git@github.com:blockworks-foundation/anchor.git, and switch to branch $ANCHOR_BRANCH!" + exit 1; +fi + # rg m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD -l | xargs -I % sed -i '' 's/m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD/5V2zCYCQkm4sZc3WctiwQEAzvfAiFxyjbwCvzQnmtmkM/g' %; WALLET_WITH_FUNDS=~/.config/solana/mango-devnet.json @@ -9,7 +16,7 @@ PROGRAM_ID=5V2zCYCQkm4sZc3WctiwQEAzvfAiFxyjbwCvzQnmtmkM # TODO fix need for --skip-lint # build program, -anchor build --skip-lint +cargo run --manifest-path ../anchor/cli/Cargo.toml build --skip-lint # patch types, which we want in rust, but anchor client doesn't support ./idl-fixup.sh @@ -22,7 +29,7 @@ if [[ -z "${NO_DEPLOY}" ]]; then solana --url https://mango.devnet.rpcpool.com program deploy --program-id 5V2zCYCQkm4sZc3WctiwQEAzvfAiFxyjbwCvzQnmtmkM -k ~/.config/solana/mango-devnet.json target/deploy/mango_v4.so # publish idl - anchor idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS --filepath target/idl/mango_v4.json $PROGRAM_ID + cargo run --manifest-path ../anchor/cli/Cargo.toml idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS --filepath target/idl/mango_v4.json $PROGRAM_ID else echo "Skipping deployment..." fi diff --git a/programs/mango-v4/Cargo.toml b/programs/mango-v4/Cargo.toml index 30d814313..22550dfcc 100644 --- a/programs/mango-v4/Cargo.toml +++ b/programs/mango-v4/Cargo.toml @@ -21,8 +21,8 @@ client = ["solana-sdk"] [dependencies] # todo: when to fix, when to use caret? need a regular chore to bump dependencies # note: possibly need init-if-needed feature -anchor-lang = { version = "0.25.0", features = [] } -anchor-spl = { version = "0.25.0", features = [] } +anchor-lang = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-spl = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } arrayref = "0.3.6" bincode = "1.3.3" bytemuck = "^1.7.2" diff --git a/programs/mango-v4/src/instructions/account_close.rs b/programs/mango-v4/src/instructions/account_close.rs index 5e49d4cce..20bc3322b 100644 --- a/programs/mango-v4/src/instructions/account_close.rs +++ b/programs/mango-v4/src/instructions/account_close.rs @@ -8,14 +8,8 @@ use crate::state::*; pub struct AccountClose<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - // note: should never be the delegate - has_one = owner, - has_one = group, - close = sol_destination - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group, has_one = owner, close = sol_destination)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] @@ -28,23 +22,24 @@ pub struct AccountClose<'info> { pub fn account_close(ctx: Context) -> Result<()> { let group = ctx.accounts.group.load()?; - // don't perform checks if group is just testing - if group.testing == 1 { - return Ok(()); - } + { + let account = ctx.accounts.account.load_mut()?; - let account = ctx.accounts.account.load()?; - require!(!account.being_liquidated(), MangoError::SomeError); - require!(!account.is_bankrupt(), MangoError::SomeError); - require_eq!(account.delegate, Pubkey::default()); - for ele in account.tokens.values { - require_eq!(ele.is_active(), false); - } - for ele in account.serum3.values { - require_eq!(ele.is_active(), false); - } - for ele in account.perps.accounts { - require_eq!(ele.is_active(), false); + // don't perform checks if group is just testing + if group.testing == 0 { + require!(!account.fixed.being_liquidated(), MangoError::SomeError); + require!(!account.fixed.is_bankrupt(), MangoError::SomeError); + require_eq!(account.fixed.delegate, Pubkey::default()); + for ele in account.token_iter() { + require_eq!(ele.is_active(), false); + } + for ele in account.serum3_iter() { + require_eq!(ele.is_active(), false); + } + for ele in account.perp_iter() { + require_eq!(ele.is_active(), false); + } + } } Ok(()) diff --git a/programs/mango-v4/src/instructions/account_create.rs b/programs/mango-v4/src/instructions/account_create.rs index 738bd5299..adea30557 100644 --- a/programs/mango-v4/src/instructions/account_create.rs +++ b/programs/mango-v4/src/instructions/account_create.rs @@ -5,7 +5,7 @@ use crate::state::*; use crate::util::fill32_from_str; #[derive(Accounts)] -#[instruction(account_num: u8)] +#[instruction(account_num: u8, account_size: AccountSize)] pub struct AccountCreate<'info> { pub group: AccountLoader<'info, Group>, @@ -14,9 +14,10 @@ pub struct AccountCreate<'info> { seeds = [group.key().as_ref(), b"MangoAccount".as_ref(), owner.key().as_ref(), &account_num.to_le_bytes()], bump, payer = payer, - space = 8 + std::mem::size_of::(), + space = MangoAccount::space(account_size), )] - pub account: AccountLoader<'info, MangoAccount>, + pub account: AccountLoaderDynamic<'info, MangoAccount>, + pub owner: Signer<'info>, #[account(mut)] @@ -25,20 +26,24 @@ pub struct AccountCreate<'info> { pub system_program: Program<'info, System>, } -pub fn account_create(ctx: Context, account_num: u8, name: String) -> Result<()> { +pub fn account_create( + ctx: Context, + account_num: u8, + account_size: AccountSize, + name: String, +) -> Result<()> { let mut account = ctx.accounts.account.load_init()?; - account.name = fill32_from_str(name)?; - account.group = ctx.accounts.group.key(); - account.owner = ctx.accounts.owner.key(); - account.account_num = account_num; - account.bump = *ctx.bumps.get("account").ok_or(MangoError::SomeError)?; - account.delegate = Pubkey::default(); - account.tokens = MangoAccountTokenPositions::default(); - account.serum3 = MangoAccountSerum3Orders::default(); - account.perps = MangoAccountPerpPositions::default(); - account.set_being_liquidated(false); - account.set_bankrupt(false); + account.fixed.name = fill32_from_str(name)?; + account.fixed.group = ctx.accounts.group.key(); + account.fixed.owner = ctx.accounts.owner.key(); + account.fixed.account_num = account_num; + account.fixed.bump = *ctx.bumps.get("account").ok_or(MangoError::SomeError)?; + account.fixed.delegate = Pubkey::default(); + account.fixed.set_being_liquidated(false); + account.fixed.set_bankrupt(false); + + account.expand_dynamic_content(account_size)?; Ok(()) } diff --git a/programs/mango-v4/src/instructions/account_edit.rs b/programs/mango-v4/src/instructions/account_edit.rs index ea0dccf56..08c37e439 100644 --- a/programs/mango-v4/src/instructions/account_edit.rs +++ b/programs/mango-v4/src/instructions/account_edit.rs @@ -8,13 +8,8 @@ use crate::util::fill32_from_str; pub struct AccountEdit<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - // Note: should never be the delegate - has_one = owner, - has_one = group, - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, } @@ -31,13 +26,11 @@ pub fn account_edit( let mut account = ctx.accounts.account.load_mut()?; - // msg!("old account {:#?}", account); - // note: unchanged fields are inline, and match exact definition in create_account // please maintain, and don't remove, makes it easy to reason about which support modification by owner if let Some(name) = name_opt { - account.name = fill32_from_str(name)?; + account.fixed.name = fill32_from_str(name)?; } // unchanged - @@ -46,7 +39,7 @@ pub fn account_edit( // bump if let Some(delegate) = delegate_opt { - account.delegate = delegate; + account.fixed.delegate = delegate; } // unchanged - @@ -56,7 +49,5 @@ pub fn account_edit( // being_liquidated // is_bankrupt - // msg!("new account {:#?}", account); - Ok(()) } diff --git a/programs/mango-v4/src/instructions/account_expand.rs b/programs/mango-v4/src/instructions/account_expand.rs new file mode 100644 index 000000000..f7cb901b6 --- /dev/null +++ b/programs/mango-v4/src/instructions/account_expand.rs @@ -0,0 +1,58 @@ +use anchor_lang::prelude::*; + +use crate::state::*; + +#[derive(Accounts)] +pub struct AccountExpand<'info> { + pub group: AccountLoader<'info, Group>, + + #[account(mut, has_one = group, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, + + pub owner: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn account_expand(ctx: Context) -> Result<()> { + let account_size = { + let account = ctx.accounts.account.load()?; + account.size() + }; + + require_eq!(account_size, AccountSize::Small); + + let new_space = MangoAccount::space(AccountSize::Large.try_into().unwrap()); + let new_rent_minimum = Rent::get()?.minimum_balance(new_space); + + let realloc_account = ctx.accounts.account.as_ref(); + let old_space = realloc_account.data_len(); + + require_gt!(new_space, old_space); + + // transfer required additional rent + anchor_lang::system_program::transfer( + anchor_lang::context::CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: realloc_account.clone(), + }, + ), + new_rent_minimum + .checked_sub(realloc_account.lamports()) + .unwrap(), + )?; + + // realloc + realloc_account.realloc(new_space, true)?; + + // expand dynamic content, e.g. to grow token positions, we need to slide serum3orders further later, and so on.... + let mut account = ctx.accounts.account.load_mut()?; + account.expand_dynamic_content(AccountSize::Large.try_into().unwrap())?; + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/compute_account_data.rs b/programs/mango-v4/src/instructions/compute_account_data.rs index e774c83be..4909e5e91 100644 --- a/programs/mango-v4/src/instructions/compute_account_data.rs +++ b/programs/mango-v4/src/instructions/compute_account_data.rs @@ -5,23 +5,21 @@ use anchor_lang::prelude::*; pub struct ComputeAccountData<'info> { pub group: AccountLoader<'info, Group>, - #[account( - has_one = group, - )] - pub account: AccountLoader<'info, MangoAccount>, + pub account: AccountLoaderDynamic<'info, MangoAccount>, } pub fn compute_account_data(ctx: Context) -> Result<()> { let group_pk = ctx.accounts.group.key(); + let account = ctx.accounts.account.load()?; let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?; - let health_cache = new_health_cache(&account, &account_retriever)?; + let health_cache = new_health_cache(&account.borrow(), &account_retriever)?; let init_health = health_cache.health(HealthType::Init); let maint_health = health_cache.health(HealthType::Maint); - let equity = compute_equity(&account, &account_retriever)?; + let equity = compute_equity(&account.borrow(), &account_retriever)?; emit!(MangoAccountData { health_cache, diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index d3d6700b8..255e9211a 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -2,8 +2,8 @@ use crate::accounts_zerocopy::*; use crate::error::MangoError; use crate::logs::{MarginTradeLog, TokenBalanceLog}; use crate::state::{ - compute_health, new_fixed_order_account_retriever, AccountRetriever, Bank, Group, HealthType, - MangoAccount, + compute_health, new_fixed_order_account_retriever, AccountLoaderDynamic, AccountRetriever, + Bank, Group, HealthType, MangoAccount, MangoAccountRefMut, }; use crate::{group_seeds, Mango}; use anchor_lang::prelude::*; @@ -29,13 +29,8 @@ use std::collections::HashMap; pub struct FlashLoan<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, - + #[account(mut, has_one = group, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, pub token_program: Program<'info, Token>, } @@ -85,7 +80,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( let group = ctx.accounts.group.load()?; let mut account = ctx.accounts.account.load_mut()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); // Go over the banks passed as health accounts and: // - Ensure that all banks that are passed in have activated positions. @@ -101,8 +96,8 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( for (i, ai) in health_ais.iter().enumerate() { match ai.load::() { Ok(bank) => { - require!(bank.group == account.group, MangoError::SomeError); - let (_, raw_token_index, _) = account.tokens.get_mut_or_create(bank.token_index)?; + require!(bank.group == account.fixed.group, MangoError::SomeError); + let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?; allowed_vaults.insert(bank.vault, (i, raw_token_index)); allowed_banks.insert(ai.key, bank); } @@ -120,8 +115,8 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( // NOTE: This health check isn't strictly necessary. It will be, later, when // we want to have reduce_only or be able to move an account out of bankruptcy. { - let retriever = new_fixed_order_account_retriever(health_ais, &account)?; - let pre_cpi_health = compute_health(&account, HealthType::Init, &retriever)?; + let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; + let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive); msg!("pre_cpi_health {:?}", pre_cpi_health); } @@ -182,7 +177,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( // Store the indexed value before the margin trade for logging purposes let mut pre_indexed_positions = Vec::new(); for (_, info) in used_vaults.iter() { - let position = account.tokens.get_raw(info.raw_token_index); + let position = account.token_get_raw(info.raw_token_index); pre_indexed_positions.push(position.indexed_position.to_bits()); } @@ -212,7 +207,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( // if there are withdraws: figure out loan amount, mark as signer if withdraw_amount > 0 { - let token_account = account.tokens.get_mut_raw(vault_info.raw_token_index); + let token_account = account.token_get_mut_raw(vault_info.raw_token_index); let native_position = token_account.native(&bank); vault_info.loan_amount = if native_position > 0 { (I80F48::from(withdraw_amount) - native_position).max(I80F48::ZERO) @@ -323,12 +318,16 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( // Track vault changes and apply them to the user's token positions let mut account = ctx.accounts.account.load_mut()?; - let inactive_tokens = - adjust_for_post_cpi_vault_amounts(health_ais, all_cpi_ais, &used_vaults, &mut account)?; + let inactive_tokens = adjust_for_post_cpi_vault_amounts( + health_ais, + all_cpi_ais, + &used_vaults, + &mut account.borrow_mut(), + )?; // Check post-cpi health - let retriever = new_fixed_order_account_retriever(health_ais, &account)?; - let post_cpi_health = compute_health(&account, HealthType::Init, &retriever)?; + let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; + let post_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; require!(post_cpi_health >= 0, MangoError::HealthMustBePositive); msg!("post_cpi_health {:?}", post_cpi_health); @@ -336,7 +335,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( let mut token_indexes = Vec::with_capacity(used_vaults.len()); let mut post_indexed_positions = Vec::with_capacity(used_vaults.len()); for (_, info) in used_vaults.iter() { - let position = account.tokens.get_raw(info.raw_token_index); + let position = account.token_get_raw(info.raw_token_index); post_indexed_positions.push(position.indexed_position.to_bits()); token_indexes.push(position.token_index as u16); @@ -365,7 +364,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( // Deactivate inactive token accounts at the end for raw_token_index in inactive_tokens { - account.tokens.deactivate(raw_token_index); + account.token_deactivate(raw_token_index); } Ok(()) @@ -375,13 +374,13 @@ fn adjust_for_post_cpi_vault_amounts( health_ais: &[AccountInfo], cpi_ais: &[AccountInfo], used_vaults: &HashMap<&Pubkey, AllowedVault>, - account: &mut MangoAccount, + account: &mut MangoAccountRefMut, ) -> Result> { let mut inactive_token_raw_indexes = Vec::with_capacity(used_vaults.len()); for (_, info) in used_vaults.iter() { let vault = Account::::try_from(&cpi_ais[info.vault_cpi_ai_index]).unwrap(); let mut bank = health_ais[info.bank_health_ai_index].load_mut::()?; - let position = account.tokens.get_mut_raw(info.raw_token_index); + let position = account.token_get_mut_raw(info.raw_token_index); let loan_origination_fee = info.loan_amount * bank.loan_origination_fee_rate; bank.collected_fees_native += loan_origination_fee; diff --git a/programs/mango-v4/src/instructions/flash_loan2.rs b/programs/mango-v4/src/instructions/flash_loan2.rs index dd3eabd1d..0171deac2 100644 --- a/programs/mango-v4/src/instructions/flash_loan2.rs +++ b/programs/mango-v4/src/instructions/flash_loan2.rs @@ -4,7 +4,7 @@ use crate::group_seeds; use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog}; use crate::state::{ compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever, - AccountRetriever, Bank, Group, HealthType, MangoAccount, TokenIndex, + AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, MangoAccount, TokenIndex, }; use crate::util::checked_math as cm; use anchor_lang::prelude::*; @@ -35,12 +35,8 @@ pub struct FlashLoan2Begin<'info> { #[derive(Accounts)] pub struct FlashLoan2End<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, pub token_program: Program<'info, Token>, @@ -162,8 +158,8 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( let group_seeds = group_seeds!(group); let mut account = ctx.accounts.account.load_mut()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); // Find index at which vaults start let vaults_index = ctx .remaining_accounts @@ -174,7 +170,7 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( return false; } - maybe_token_account.unwrap().owner == account.group + maybe_token_account.unwrap().owner == account.fixed.group }) .ok_or_else(|| error!(MangoError::SomeError))?; @@ -213,7 +209,7 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( require_neq!(bank.flash_loan_vault_initial, u64::MAX); // Create the token position now, so we can compute the pre-health with fixed order health accounts - let (_, raw_token_index, _) = account.tokens.get_mut_or_create(bank.token_index)?; + let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?; // Revoke delegation let ix = token::spl_token::instruction::revoke( @@ -246,8 +242,8 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( // Check pre-cpi health // NOTE: This health check isn't strictly necessary. It will be, later, when // we want to have reduce_only or be able to move an account out of bankruptcy. - let retriever = new_fixed_order_account_retriever(health_ais, &account)?; - let pre_cpi_health = compute_health(&account, HealthType::Init, &retriever)?; + let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; + let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive); msg!("pre_cpi_health {:?}", pre_cpi_health); @@ -255,7 +251,7 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( let mut prices = vec![]; for change in &changes { let (_, oracle_price) = retriever.bank_and_oracle( - &account.group, + &account.fixed.group, change.bank_index, change.raw_token_index as TokenIndex, )?; @@ -270,7 +266,7 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( let mut token_loan_details = Vec::with_capacity(changes.len()); for (change, price) in changes.iter().zip(prices.iter()) { let mut bank = health_ais[change.bank_index].load_mut::()?; - let position = account.tokens.get_mut_raw(change.raw_token_index); + let position = account.token_get_mut_raw(change.raw_token_index); let native = position.native(&bank); let approved_amount = I80F48::from(bank.flash_loan_approved_amount); @@ -314,18 +310,18 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>( emit!(FlashLoanLog { mango_account: ctx.accounts.account.key(), - token_loan_details: token_loan_details + token_loan_details }); // Check post-cpi health let post_cpi_health = - compute_health_from_fixed_accounts(&account, HealthType::Init, health_ais)?; + compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?; require!(post_cpi_health >= 0, MangoError::HealthMustBePositive); msg!("post_cpi_health {:?}", post_cpi_health); // Deactivate inactive token accounts after health check for raw_token_index in deactivated_token_positions { - account.tokens.deactivate(raw_token_index); + account.token_deactivate(raw_token_index); } Ok(()) diff --git a/programs/mango-v4/src/instructions/flash_loan3.rs b/programs/mango-v4/src/instructions/flash_loan3.rs index 35e853318..caf47fa70 100644 --- a/programs/mango-v4/src/instructions/flash_loan3.rs +++ b/programs/mango-v4/src/instructions/flash_loan3.rs @@ -2,9 +2,10 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::group_seeds; use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog}; +use crate::state::MangoAccount; use crate::state::{ compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever, - AccountRetriever, Bank, Group, HealthType, MangoAccount, TokenIndex, + AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, TokenIndex, }; use crate::util::checked_math as cm; use anchor_lang::prelude::*; @@ -38,11 +39,8 @@ pub struct FlashLoan3Begin<'info> { /// the `owner` must have authority to transfer tokens out of them #[derive(Accounts)] pub struct FlashLoan3End<'info> { - #[account( - mut, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, pub token_program: Program<'info, Token>, @@ -179,7 +177,8 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>, ) -> Result<()> { let mut account = ctx.accounts.account.load_mut()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); // Find index at which vaults start let vaults_index = ctx @@ -191,7 +190,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( return false; } - maybe_token_account.unwrap().owner == account.group + maybe_token_account.unwrap().owner == account.fixed.group }) .ok_or_else(|| error_msg!("expected at least one vault token account to be passed"))?; let vaults_len = (ctx.remaining_accounts.len() - vaults_index) / 2; @@ -234,7 +233,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( require_neq!(bank.flash_loan_vault_initial, u64::MAX); // Create the token position now, so we can compute the pre-health with fixed order health accounts - let (_, raw_token_index, _) = account.tokens.get_mut_or_create(bank.token_index)?; + let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?; // Transfer any excess over the inital balance of the token account back // into the vault. Compute the total change in the vault balance. @@ -276,16 +275,19 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( // Check pre-cpi health // NOTE: This health check isn't strictly necessary. It will be, later, when // we want to have reduce_only or be able to move an account out of bankruptcy. - let retriever = new_fixed_order_account_retriever(health_ais, &account)?; - let pre_cpi_health = compute_health(&account, HealthType::Init, &retriever)?; + let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; + let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive); msg!("pre_cpi_health {:?}", pre_cpi_health); // Prices for logging let mut prices = vec![]; for change in &changes { - let (_, oracle_price) = - retriever.bank_and_oracle(&account.group, change.bank_index, change.token_index)?; + let (_, oracle_price) = retriever.bank_and_oracle( + &account.fixed.group, + change.bank_index, + change.token_index, + )?; prices.push(oracle_price); } @@ -297,7 +299,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( let mut token_loan_details = Vec::with_capacity(changes.len()); for (change, price) in changes.iter().zip(prices.iter()) { let mut bank = health_ais[change.bank_index].load_mut::()?; - let position = account.tokens.get_mut_raw(change.raw_token_index); + let position = account.token_get_mut_raw(change.raw_token_index); let native = position.native(&bank); let approved_amount = I80F48::from(bank.flash_loan_approved_amount); @@ -341,18 +343,18 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( emit!(FlashLoanLog { mango_account: ctx.accounts.account.key(), - token_loan_details: token_loan_details + token_loan_details }); // Check post-cpi health let post_cpi_health = - compute_health_from_fixed_accounts(&account, HealthType::Init, health_ais)?; + compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?; require!(post_cpi_health >= 0, MangoError::HealthMustBePositive); msg!("post_cpi_health {:?}", post_cpi_health); // Deactivate inactive token accounts after health check for raw_token_index in deactivated_token_positions { - account.tokens.deactivate(raw_token_index); + account.token_deactivate(raw_token_index); } Ok(()) diff --git a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs index fcd0857fb..eee966613 100644 --- a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs @@ -21,19 +21,12 @@ pub struct LiqTokenBankruptcy<'info> { )] pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()), - )] - pub liqor: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub liqor: AccountLoaderDynamic<'info, MangoAccount>, pub liqor_owner: Signer<'info>, - #[account( - mut, - has_one = group, - )] - pub liqee: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub liqee: AccountLoaderDynamic<'info, MangoAccount>, #[account( has_one = group, @@ -80,14 +73,20 @@ pub fn liq_token_bankruptcy( ); let mut liqor = ctx.accounts.liqor.load_mut()?; - require!(!liqor.is_bankrupt(), MangoError::IsBankrupt); + require!( + liqor + .fixed + .is_owner_or_delegate(ctx.accounts.liqor_owner.key()), + MangoError::SomeError + ); + require!(!liqor.fixed.is_bankrupt(), MangoError::IsBankrupt); let mut liqee = ctx.accounts.liqee.load_mut()?; - require!(liqee.is_bankrupt(), MangoError::IsNotBankrupt); + require!(liqee.fixed.is_bankrupt(), MangoError::IsBankrupt); let liab_bank = bank_ais[0].load::()?; let liab_deposit_index = liab_bank.deposit_index; - let (liqee_liab, liqee_raw_token_index) = liqee.tokens.get_mut(liab_token_index)?; + let (liqee_liab, liqee_raw_token_index) = liqee.token_get_mut(liab_token_index)?; let mut remaining_liab_loss = -liqee_liab.native(&liab_bank); require_gt!(remaining_liab_loss, I80F48::ZERO); drop(liab_bank); @@ -140,23 +139,24 @@ pub fn liq_token_bankruptcy( // credit the liqor let (liqor_quote, liqor_quote_raw_token_index, _) = - liqor.tokens.get_mut_or_create(QUOTE_TOKEN_INDEX)?; + liqor.token_get_mut_or_create(QUOTE_TOKEN_INDEX)?; let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; // transfer liab from liqee to liqor let (liqor_liab, liqor_liab_raw_token_index, _) = - liqor.tokens.get_mut_or_create(liab_token_index)?; + liqor.token_get_mut_or_create(liab_token_index)?; let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?; // Check liqor's health - let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever)?; + let liqor_health = + compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); if !liqor_quote_active { - liqor.tokens.deactivate(liqor_quote_raw_token_index); + liqor.token_deactivate(liqor_quote_raw_token_index); } if !liqor_liab_active { - liqor.tokens.deactivate(liqor_liab_raw_token_index); + liqor.token_deactivate(liqor_liab_raw_token_index); } } else { // For liab_token_index == QUOTE_TOKEN_INDEX: the insurance fund deposits directly into liqee, @@ -207,14 +207,14 @@ pub fn liq_token_bankruptcy( // If the account has no more borrows then it's no longer bankrupt // and should (always?) no longer be liquidated. let account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?; - let liqee_health_cache = new_health_cache(&liqee, &account_retriever)?; - liqee.set_bankrupt(liqee_health_cache.has_borrows()); + let liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)?; + liqee.fixed.set_bankrupt(liqee_health_cache.has_borrows()); if !liqee.is_bankrupt() && liqee_health_cache.health(HealthType::Init) >= 0 { - liqee.set_being_liquidated(false); + liqee.fixed.set_being_liquidated(false); } if !liqee_liab_active { - liqee.tokens.deactivate(liqee_raw_token_index); + liqee.token_deactivate(liqee_raw_token_index); } Ok(()) diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index 953a5f4a7..433829d8f 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -12,19 +12,12 @@ use crate::util::checked_math as cm; pub struct LiqTokenWithToken<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()), - )] - pub liqor: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub liqor: AccountLoaderDynamic<'info, MangoAccount>, pub liqor_owner: Signer<'info>, - #[account( - mut, - has_one = group, - )] - pub liqee: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub liqee: AccountLoaderDynamic<'info, MangoAccount>, } pub fn liq_token_with_token( @@ -40,30 +33,34 @@ pub fn liq_token_with_token( .context("create account retriever")?; let mut liqor = ctx.accounts.liqor.load_mut()?; - require!(!liqor.is_bankrupt(), MangoError::IsBankrupt); + require!( + liqor + .fixed + .is_owner_or_delegate(ctx.accounts.liqor_owner.key()), + MangoError::SomeError + ); + require!(!liqor.fixed.is_bankrupt(), MangoError::IsBankrupt); let mut liqee = ctx.accounts.liqee.load_mut()?; - require!(!liqee.is_bankrupt(), MangoError::IsBankrupt); + require!(!liqee.fixed.is_bankrupt(), MangoError::IsBankrupt); // Initial liqee health check - let mut liqee_health_cache = - new_health_cache(&liqee, &account_retriever).context("create liqee health cache")?; + let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever) + .context("create liqee health cache")?; let init_health = liqee_health_cache.health(HealthType::Init); - msg!("init health: {}", init_health); if liqee.being_liquidated() { if init_health > I80F48::ZERO { - liqee.set_being_liquidated(false); + liqee.fixed.set_being_liquidated(false); msg!("Liqee init_health above zero"); return Ok(()); } } else { let maint_health = liqee_health_cache.health(HealthType::Maint); - msg!("maint health: {}", maint_health); require!( maint_health < I80F48::ZERO, MangoError::HealthMustBeNegative ); - liqee.set_being_liquidated(true); + liqee.fixed.set_being_liquidated(true); } // @@ -81,11 +78,11 @@ pub fn liq_token_with_token( // The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position // borrows alive at the same time. Possibly adding get_mut_pair() would be helpful. - let (liqee_asset_position, liqee_asset_raw_index) = liqee.tokens.get(asset_token_index)?; + let (liqee_asset_position, liqee_asset_raw_index) = liqee.token_get(asset_token_index)?; let liqee_assets_native = liqee_asset_position.native(&asset_bank); require!(liqee_assets_native.is_positive(), MangoError::SomeError); - let (liqee_liab_position, liqee_liab_raw_index) = liqee.tokens.get(liab_token_index)?; + let (liqee_liab_position, liqee_liab_raw_index) = liqee.token_get(liab_token_index)?; let liqee_liab_native = liqee_liab_position.native(&liab_bank); require!(liqee_liab_native.is_negative(), MangoError::SomeError); @@ -124,21 +121,21 @@ pub fn liq_token_with_token( let asset_transfer = cm!(liab_transfer * liab_price_adjusted / asset_price); // Apply the balance changes to the liqor and liqee accounts - let liqee_liab_position = liqee.tokens.get_mut_raw(liqee_liab_raw_index); + let liqee_liab_position = liqee.token_get_mut_raw(liqee_liab_raw_index); let liqee_liab_active = liab_bank.deposit(liqee_liab_position, liab_transfer)?; let liqee_liab_position_indexed = liqee_liab_position.indexed_position; let (liqor_liab_position, liqor_liab_raw_index, _) = - liqor.tokens.get_mut_or_create(liab_token_index)?; + liqor.token_get_mut_or_create(liab_token_index)?; let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; let liqor_liab_position_indexed = liqor_liab_position.indexed_position; let (liqor_asset_position, liqor_asset_raw_index, _) = - liqor.tokens.get_mut_or_create(asset_token_index)?; + liqor.token_get_mut_or_create(asset_token_index)?; let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?; let liqor_asset_position_indexed = liqor_asset_position.indexed_position; - let liqee_asset_position = liqee.tokens.get_mut_raw(liqee_asset_raw_index); + let liqee_asset_position = liqee.token_get_mut_raw(liqee_asset_raw_index); let liqee_asset_active = asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?; let liqee_asset_position_indexed = liqee_asset_position.indexed_position; @@ -156,8 +153,8 @@ pub fn liq_token_with_token( emit!(LiquidateTokenAndTokenLog { liqee: ctx.accounts.liqee.key(), liqor: ctx.accounts.liqor.key(), - asset_token_index: asset_token_index, - liab_token_index: liab_token_index, + asset_token_index, + liab_token_index, asset_transfer: asset_transfer.to_bits(), liab_transfer: liab_transfer.to_bits(), asset_price: asset_price.to_bits(), @@ -204,33 +201,35 @@ pub fn liq_token_with_token( // Since we use a scanning account retriever, it's safe to deactivate inactive token positions if !liqee_asset_active { - liqee.tokens.deactivate(liqee_asset_raw_index); + liqee.token_deactivate(liqee_asset_raw_index); } if !liqee_liab_active { - liqee.tokens.deactivate(liqee_liab_raw_index); + liqee.token_deactivate(liqee_liab_raw_index); } if !liqor_asset_active { - liqor.tokens.deactivate(liqor_asset_raw_index); + liqor.token_deactivate(liqor_asset_raw_index); } if !liqor_liab_active { - liqor.tokens.deactivate(liqor_liab_raw_index) + liqor.token_deactivate(liqor_liab_raw_index) } } // Check liqee health again let maint_health = liqee_health_cache.health(HealthType::Maint); if maint_health < I80F48::ZERO { - liqee.set_bankrupt(!liqee_health_cache.has_liquidatable_assets()); + liqee + .fixed + .set_bankrupt(!liqee_health_cache.has_liquidatable_assets()); } else { let init_health = liqee_health_cache.health(HealthType::Init); // this is equivalent to one native USDC or 1e-6 USDC // This is used as threshold to flip flag instead of 0 because of dust issues - liqee.set_being_liquidated(init_health < -I80F48::ONE); + liqee.fixed.set_being_liquidated(init_health < -I80F48::ONE); } // Check liqor's health - let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever) + let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index c2845c2ee..b9ac42769 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -1,6 +1,7 @@ pub use account_close::*; pub use account_create::*; pub use account_edit::*; +pub use account_expand::*; pub use benchmark::*; pub use compute_account_data::*; pub use flash_loan::*; @@ -43,6 +44,7 @@ pub use token_withdraw::*; mod account_close; mod account_create; mod account_edit; +mod account_expand; mod benchmark; mod compute_account_data; mod flash_loan; diff --git a/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs b/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs index 74de468e5..0ffc2c7c9 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_all_orders.rs @@ -1,18 +1,14 @@ use anchor_lang::prelude::*; use crate::error::MangoError; -use crate::state::{Book, BookSide, Group, MangoAccount, PerpMarket}; +use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket}; #[derive(Accounts)] pub struct PerpCancelAllOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -29,15 +25,20 @@ pub struct PerpCancelAllOrders<'info> { } pub fn perp_cancel_all_orders(ctx: Context, limit: u8) -> Result<()> { - let mut mango_account = ctx.accounts.account.load_mut()?; - require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt); + let mut account = ctx.accounts.account.load_mut()?; + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let mut perp_market = ctx.accounts.perp_market.load_mut()?; let bids = ctx.accounts.bids.load_mut()?; let asks = ctx.accounts.asks.load_mut()?; let mut book = Book::new(bids, asks); - book.cancel_all_order(&mut mango_account, &mut perp_market, limit, None)?; + book.cancel_all_order(&mut account.borrow_mut(), &mut perp_market, limit, None)?; Ok(()) } diff --git a/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs b/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs index 58c8001c9..118108fe3 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_all_orders_by_side.rs @@ -1,18 +1,14 @@ use anchor_lang::prelude::*; use crate::error::MangoError; -use crate::state::{Book, BookSide, Group, MangoAccount, PerpMarket, Side}; +use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket, Side}; #[derive(Accounts)] pub struct PerpCancelAllOrdersBySide<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -33,15 +29,26 @@ pub fn perp_cancel_all_orders_by_side( side_option: Option, limit: u8, ) -> Result<()> { - let mut mango_account = ctx.accounts.account.load_mut()?; - require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt); + let mut account = ctx.accounts.account.load_mut()?; + require_keys_eq!(account.fixed.group, ctx.accounts.group.key()); + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let mut perp_market = ctx.accounts.perp_market.load_mut()?; let bids = ctx.accounts.bids.load_mut()?; let asks = ctx.accounts.asks.load_mut()?; let mut book = Book::new(bids, asks); - book.cancel_all_order(&mut mango_account, &mut perp_market, limit, side_option)?; + book.cancel_all_order( + &mut account.borrow_mut(), + &mut perp_market, + limit, + side_option, + )?; Ok(()) } diff --git a/programs/mango-v4/src/instructions/perp_cancel_order.rs b/programs/mango-v4/src/instructions/perp_cancel_order.rs index c1c82f2dc..6c7c4c2d9 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_order.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_order.rs @@ -1,18 +1,14 @@ use anchor_lang::prelude::*; use crate::error::MangoError; -use crate::state::{Book, BookSide, Group, MangoAccount, PerpMarket}; +use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket}; #[derive(Accounts)] pub struct PerpCancelOrder<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -29,17 +25,21 @@ pub struct PerpCancelOrder<'info> { } pub fn perp_cancel_order(ctx: Context, order_id: i128) -> Result<()> { - let mut mango_account = ctx.accounts.account.load_mut()?; - require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt); + let mut account = ctx.accounts.account.load_mut()?; + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let perp_market = ctx.accounts.perp_market.load_mut()?; let bids = ctx.accounts.bids.load_mut()?; let asks = ctx.accounts.asks.load_mut()?; let mut book = Book::new(bids, asks); - let side = mango_account - .perps - .find_order_side(perp_market.perp_market_index, order_id) + let side = account + .perp_find_order_side(perp_market.perp_market_index, order_id) .ok_or_else(|| error!(MangoError::SomeError))?; // InvalidOrderId let order = book.cancel_order(order_id, side)?; @@ -48,7 +48,5 @@ pub fn perp_cancel_order(ctx: Context, order_id: i128) -> Resul MangoError::SomeError // InvalidOwner ); - mango_account - .perps - .remove_order(order.owner_slot as usize, order.quantity) + account.perp_remove_order(order.owner_slot as usize, order.quantity) } diff --git a/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs b/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs index 7c1101bee..e17953880 100644 --- a/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs +++ b/programs/mango-v4/src/instructions/perp_cancel_order_by_client_order_id.rs @@ -1,18 +1,14 @@ use anchor_lang::prelude::*; use crate::error::MangoError; -use crate::state::{Book, BookSide, Group, MangoAccount, PerpMarket}; +use crate::state::{AccountLoaderDynamic, Book, BookSide, Group, MangoAccount, PerpMarket}; #[derive(Accounts)] pub struct PerpCancelOrderByClientOrderId<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -32,17 +28,21 @@ pub fn perp_cancel_order_by_client_order_id( ctx: Context, client_order_id: u64, ) -> Result<()> { - let mut mango_account = ctx.accounts.account.load_mut()?; - require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt); + let mut account = ctx.accounts.account.load_mut()?; + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let perp_market = ctx.accounts.perp_market.load_mut()?; let bids = ctx.accounts.bids.load_mut()?; let asks = ctx.accounts.asks.load_mut()?; let mut book = Book::new(bids, asks); - let (order_id, side) = mango_account - .perps - .find_order_with_client_order_id(perp_market.perp_market_index, client_order_id) + let (order_id, side) = account + .perp_find_order_with_client_order_id(perp_market.perp_market_index, client_order_id) .ok_or_else(|| error!(MangoError::SomeError))?; let order = book.cancel_order(order_id, side)?; @@ -51,7 +51,5 @@ pub fn perp_cancel_order_by_client_order_id( MangoError::SomeError // InvalidOwner ); - mango_account - .perps - .remove_order(order.owner_slot as usize, order.quantity) + account.perp_remove_order(order.owner_slot as usize, order.quantity) } diff --git a/programs/mango-v4/src/instructions/perp_consume_events.rs b/programs/mango-v4/src/instructions/perp_consume_events.rs index 310b24f9f..4bb7197f0 100644 --- a/programs/mango-v4/src/instructions/perp_consume_events.rs +++ b/programs/mango-v4/src/instructions/perp_consume_events.rs @@ -1,10 +1,9 @@ use anchor_lang::prelude::*; use bytemuck::cast_ref; -use crate::accounts_zerocopy::*; use crate::error::MangoError; -use crate::state::EventQueue; -use crate::state::{EventType, FillEvent, Group, MangoAccount, OutEvent, PerpMarket}; +use crate::state::{AccountLoaderDynamic, EventQueue, MangoAccount}; +use crate::state::{EventType, FillEvent, Group, OutEvent, PerpMarket}; use crate::logs::{emit_perp_balances, FillLog}; @@ -41,73 +40,88 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res // handle self trade separately because of rust borrow checker if fill.maker == fill.taker { - let mut ma = match mango_account_ais.iter().find(|ai| ai.key == &fill.maker) { + match mango_account_ais.iter().find(|ai| ai.key == &fill.maker) { None => { msg!("Unable to find account {}", fill.maker.to_string()); return Ok(()); } - Some(account_info) => account_info.load_mut::()?, - }; - ma.perps.execute_maker( - perp_market.perp_market_index, - &mut perp_market, - fill, - )?; - ma.perps.execute_taker( - perp_market.perp_market_index, - &mut perp_market, - fill, - )?; - emit_perp_balances( - fill.maker, - perp_market.perp_market_index as u64, - fill.price, - &ma.perps.accounts[perp_market.perp_market_index as usize], - &perp_market, - ); + Some(ai) => { + let mal: AccountLoaderDynamic = + AccountLoaderDynamic::try_from(&ai)?; + let mut ma = mal.load_mut()?; + ma.perp_execute_maker( + perp_market.perp_market_index, + &mut perp_market, + fill, + )?; + ma.perp_execute_taker( + perp_market.perp_market_index, + &mut perp_market, + fill, + )?; + emit_perp_balances( + fill.maker, + perp_market.perp_market_index as u64, + fill.price, + &ma.perp_find_account(perp_market.perp_market_index).unwrap(), + &perp_market, + ); + } + }; } else { - let mut maker = match mango_account_ais.iter().find(|ai| ai.key == &fill.maker) - { + match mango_account_ais.iter().find(|ai| ai.key == &fill.maker) { None => { msg!("Unable to find maker account {}", fill.maker.to_string()); return Ok(()); } - Some(account_info) => account_info.load_mut::()?, - }; - let mut taker = match mango_account_ais.iter().find(|ai| ai.key == &fill.taker) - { - None => { - msg!("Unable to find taker account {}", fill.taker.to_string()); - return Ok(()); - } - Some(account_info) => account_info.load_mut::()?, - }; + Some(ai) => { + let mal: AccountLoaderDynamic = + AccountLoaderDynamic::try_from(&ai)?; + let mut maker = mal.load_mut()?; - maker.perps.execute_maker( - perp_market.perp_market_index, - &mut perp_market, - fill, - )?; - taker.perps.execute_taker( - perp_market.perp_market_index, - &mut perp_market, - fill, - )?; - emit_perp_balances( - fill.maker, - perp_market.perp_market_index as u64, - fill.price, - &maker.perps.accounts[perp_market.perp_market_index as usize], - &perp_market, - ); - emit_perp_balances( - fill.taker, - perp_market.perp_market_index as u64, - fill.price, - &taker.perps.accounts[perp_market.perp_market_index as usize], - &perp_market, - ); + match mango_account_ais.iter().find(|ai| ai.key == &fill.taker) { + None => { + msg!("Unable to find taker account {}", fill.taker.to_string()); + return Ok(()); + } + Some(ai) => { + let mal: AccountLoaderDynamic = + AccountLoaderDynamic::try_from(&ai)?; + let mut taker = mal.load_mut()?; + + maker.perp_execute_maker( + perp_market.perp_market_index, + &mut perp_market, + fill, + )?; + taker.perp_execute_taker( + perp_market.perp_market_index, + &mut perp_market, + fill, + )?; + emit_perp_balances( + fill.maker, + perp_market.perp_market_index as u64, + fill.price, + &maker + .perp_find_account(perp_market.perp_market_index) + .unwrap(), + &perp_market, + ); + emit_perp_balances( + fill.taker, + perp_market.perp_market_index as u64, + fill.price, + &taker + .perp_find_account(perp_market.perp_market_index) + .unwrap(), + &perp_market, + ); + } + }; + } + }; } emit!(FillLog { mango_group: ctx.accounts.group.key(), @@ -134,16 +148,19 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res EventType::Out => { let out: &OutEvent = cast_ref(event); - let mut ma = match mango_account_ais.iter().find(|ai| ai.key == &out.owner) { + match mango_account_ais.iter().find(|ai| ai.key == &out.owner) { None => { msg!("Unable to find account {}", out.owner.to_string()); return Ok(()); } - Some(account_info) => account_info.load_mut::()?, - }; + Some(ai) => { + let mal: AccountLoaderDynamic = + AccountLoaderDynamic::try_from(&ai)?; + let mut ma = mal.load_mut()?; - ma.perps - .remove_order(out.owner_slot as usize, out.quantity)?; + ma.perp_remove_order(out.owner_slot as usize, out.quantity)?; + } + }; } EventType::Liquidate => { // This is purely for record keeping. Can be removed if program logs are superior diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index cbe546d37..1781c55e4 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -2,21 +2,18 @@ use anchor_lang::prelude::*; use crate::accounts_zerocopy::*; use crate::error::*; +use crate::state::MangoAccount; use crate::state::{ - compute_health, new_fixed_order_account_retriever, oracle_price, Book, BookSide, EventQueue, - Group, HealthType, MangoAccount, OrderType, PerpMarket, Side, + compute_health, new_fixed_order_account_retriever, oracle_price, AccountLoaderDynamic, Book, + BookSide, EventQueue, Group, HealthType, OrderType, PerpMarket, Side, }; #[derive(Accounts)] pub struct PerpPlaceOrder<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -78,9 +75,14 @@ pub fn perp_place_order( // When the limit is reached, processing stops and the instruction succeeds. limit: u8, ) -> Result<()> { - let mut mango_account = ctx.accounts.account.load_mut()?; - require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt); - let mango_account_pk = ctx.accounts.account.key(); + let mut account = ctx.accounts.account.load_mut()?; + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); + let account_pk = ctx.accounts.account.key(); { let mut perp_market = ctx.accounts.perp_market.load_mut()?; @@ -118,8 +120,8 @@ pub fn perp_place_order( &mut perp_market, &mut event_queue, oracle_price, - &mut mango_account.perps, - &mango_account_pk, + &mut account.borrow_mut(), + &account_pk, price_lots, max_base_lots, max_quote_lots, @@ -131,8 +133,8 @@ pub fn perp_place_order( )?; } - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &mango_account)?; - let health = compute_health(&mango_account, HealthType::Init, &retriever)?; + let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; msg!("health: {}", health); require!(health >= 0, MangoError::HealthMustBePositive); diff --git a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs index 1771446ff..c7814aaa2 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs @@ -7,12 +7,8 @@ use crate::state::*; pub struct Serum3CancelAllOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] @@ -50,15 +46,19 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) // { let account = ctx.accounts.account.load()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let serum_market = ctx.accounts.serum_market.load()?; // Validate open_orders require!( account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .ok_or_else(|| error!(MangoError::SomeError))? .open_orders == ctx.accounts.open_orders.key(), diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index 2cbee5170..546bbd583 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -14,12 +14,8 @@ use checked_math as cm; pub struct Serum3CancelOrder<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] @@ -63,13 +59,17 @@ pub fn serum3_cancel_order( // { let account = ctx.accounts.account.load()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); // Validate open_orders require!( account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .ok_or_else(|| error!(MangoError::SomeError))? .open_orders == ctx.accounts.open_orders.key(), @@ -96,7 +96,7 @@ pub fn serum3_cancel_order( let mut account = ctx.accounts.account.load_mut()?; decrease_maybe_loan( serum_market.market_index, - &mut account, + &mut account.borrow_mut(), &before_oo, &after_oo, ); @@ -109,11 +109,11 @@ pub fn serum3_cancel_order( // the cached pub fn decrease_maybe_loan( market_index: Serum3MarketIndex, - account: &mut MangoAccount, + account: &mut MangoAccountRefMut, before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim, ) { - let serum3_account = account.serum3.find_mut(market_index).unwrap(); + let serum3_account = account.serum3_find_mut(market_index).unwrap(); if after_oo.native_coin_free > before_oo.native_coin_free { let native_coin_free_increase = after_oo.native_coin_free - before_oo.native_coin_free; diff --git a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs index cf8a44969..db395787c 100644 --- a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs @@ -7,12 +7,8 @@ use crate::state::*; pub struct Serum3CloseOpenOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -40,13 +36,19 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( // Validation // let mut account = ctx.accounts.account.load_mut()?; + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); + let serum_market = ctx.accounts.serum_market.load()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + // Validate open_orders require!( account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .ok_or_else(|| error!(MangoError::SomeError))? .open_orders == ctx.accounts.open_orders.key(), @@ -59,7 +61,7 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( cpi_close_open_orders(ctx.accounts)?; // TODO: decrement in_use_count on the base token and quote token - account.serum3.deactivate(serum_market.market_index)?; + account.serum3_deactivate(serum_market.market_index)?; Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_create_open_orders.rs b/programs/mango-v4/src/instructions/serum3_create_open_orders.rs index ddf24a6e2..45b247a92 100644 --- a/programs/mango-v4/src/instructions/serum3_create_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_create_open_orders.rs @@ -7,12 +7,8 @@ use crate::state::*; pub struct Serum3CreateOpenOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -50,9 +46,16 @@ pub fn serum3_create_open_orders(ctx: Context) -> Result cpi_init_open_orders(ctx.accounts)?; let serum_market = ctx.accounts.serum_market.load()?; + let mut account = ctx.accounts.account.load_mut()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); - let serum_account = account.serum3.create(serum_market.market_index)?; + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); + + let serum_account = account.serum3_create(serum_market.market_index)?; serum_account.open_orders = ctx.accounts.open_orders.key(); serum_account.base_token_index = serum_market.base_token_index; serum_account.quote_token_index = serum_market.quote_token_index; @@ -60,13 +63,9 @@ pub fn serum3_create_open_orders(ctx: Context) -> Result // Make it so that the token_account_map for the base and quote currency // stay permanently blocked. Otherwise users may end up in situations where // they can't settle a market because they don't have free token_account_map! - let (quote_position, _, _) = account - .tokens - .get_mut_or_create(serum_market.quote_token_index)?; + let (quote_position, _, _) = account.token_get_mut_or_create(serum_market.quote_token_index)?; quote_position.in_use_count += 1; - let (base_position, _, _) = account - .tokens - .get_mut_or_create(serum_market.base_token_index)?; + let (base_position, _, _) = account.token_get_mut_or_create(serum_market.base_token_index)?; base_position.in_use_count += 1; Ok(()) diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 172ce19c2..eaa56e5c8 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -9,11 +9,8 @@ use crate::state::*; pub struct Serum3LiqForceCancelOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, #[account(mut)] /// CHECK: Validated inline by checking against the pubkey stored in the account @@ -73,14 +70,13 @@ pub fn serum3_liq_force_cancel_orders( // { let account = ctx.accounts.account.load()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let serum_market = ctx.accounts.serum_market.load()?; // Validate open_orders require!( account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .ok_or_else(|| error!(MangoError::SomeError))? .open_orders == ctx.accounts.open_orders.key(), @@ -112,8 +108,9 @@ pub fn serum3_liq_force_cancel_orders( { let account = ctx.accounts.account.load()?; - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?; - let health = compute_health(&account, HealthType::Maint, &retriever)?; + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Maint, &retriever)?; msg!("health: {}", health); require!(health < 0, MangoError::SomeError); } @@ -143,7 +140,7 @@ pub fn serum3_liq_force_cancel_orders( let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( - &mut account, + &mut account.borrow_mut(), &mut base_bank, after_base_vault, before_base_vault, @@ -151,7 +148,7 @@ pub fn serum3_liq_force_cancel_orders( after_quote_vault, before_quote_vault, )? - .deactivate_inactive_token_accounts(&mut account); + .deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 60beae834..3ee9a6f86 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -83,12 +83,8 @@ pub enum Serum3Side { pub struct Serum3PlaceOrder<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] @@ -168,13 +164,16 @@ pub fn serum3_place_order( // { let account = ctx.accounts.account.load()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); // Validate open_orders require!( account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .ok_or_else(|| error!(MangoError::SomeError))? .open_orders == ctx.accounts.open_orders.key(), @@ -257,7 +256,7 @@ pub fn serum3_place_order( let mut account = ctx.accounts.account.load_mut()?; inc_maybe_loan( serum_market.market_index, - &mut account, + &mut account.borrow_mut(), &before_oo, &after_oo, ); @@ -279,7 +278,7 @@ pub fn serum3_place_order( let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( - &mut account, + &mut account.borrow_mut(), &mut base_bank, after_base_vault, before_base_vault, @@ -292,12 +291,12 @@ pub fn serum3_place_order( // // Health check // - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?; - let health = compute_health(&account, HealthType::Init, &retriever)?; + let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; msg!("health: {}", health); require!(health >= 0, MangoError::HealthMustBePositive); - vault_difference_result.deactivate_inactive_token_accounts(&mut account); + vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } @@ -305,11 +304,11 @@ pub fn serum3_place_order( // if reserved has increased, then increase cached value by the increase in reserved pub fn inc_maybe_loan( market_index: Serum3MarketIndex, - account: &mut MangoAccount, + account: &mut MangoAccountRefMut, before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim, ) { - let serum3_account = account.serum3.find_mut(market_index).unwrap(); + let serum3_account = account.serum3_find_mut(market_index).unwrap(); if after_oo.native_coin_reserved() > before_oo.native_coin_reserved() { let native_coin_reserved_increase = @@ -333,18 +332,18 @@ pub struct VaultDifferenceResult { } impl VaultDifferenceResult { - pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccount) { + pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccountRefMut) { if !self.base_active { - account.tokens.deactivate(self.base_raw_index); + account.token_deactivate(self.base_raw_index); } if !self.quote_active { - account.tokens.deactivate(self.quote_raw_index); + account.token_deactivate(self.quote_raw_index); } } } pub fn apply_vault_difference( - account: &mut MangoAccount, + account: &mut MangoAccountRefMut, base_bank: &mut Bank, after_base_vault: u64, before_base_vault: u64, @@ -356,11 +355,11 @@ pub fn apply_vault_difference( // charged if an order executes and the loan materializes? Otherwise MMs that place // an order without having the funds will be charged for each place_order! - let (base_position, base_raw_index) = account.tokens.get_mut(base_bank.token_index)?; + let (base_position, base_raw_index) = account.token_get_mut(base_bank.token_index)?; let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault); let base_active = base_bank.change_with_fee(base_position, base_change)?; - let (quote_position, quote_raw_index) = account.tokens.get_mut(quote_bank.token_index)?; + let (quote_position, quote_raw_index) = account.token_get_mut(quote_bank.token_index)?; let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault); let quote_active = quote_bank.change_with_fee(quote_position, quote_change)?; diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index c9e784c5b..4632d6a84 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -15,12 +15,8 @@ use super::{apply_vault_difference, OpenOrdersReserved, OpenOrdersSlim}; pub struct Serum3SettleFunds<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - constraint = account.load()?.is_owner_or_delegate(owner.key()), - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] @@ -77,13 +73,17 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { // { let account = ctx.accounts.account.load()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); // Validate open_orders require!( account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .ok_or_else(|| error!(MangoError::SomeError))? .open_orders == ctx.accounts.open_orders.key(), @@ -126,15 +126,14 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { cpi_settle_funds(ctx.accounts)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); - let account = &mut ctx.accounts.account.load_mut()?; - + let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; charge_maybe_fees( serum_market.market_index, &mut base_bank, &mut quote_bank, - account, + &mut account.borrow_mut(), &after_oo, )?; } @@ -153,7 +152,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; apply_vault_difference( - &mut account, + &mut account.borrow_mut(), &mut base_bank, after_base_vault, before_base_vault, @@ -161,7 +160,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { after_quote_vault, before_quote_vault, )? - .deactivate_inactive_token_accounts(&mut account); + .deactivate_inactive_token_accounts(&mut account.borrow_mut()); } Ok(()) @@ -172,10 +171,10 @@ pub fn charge_maybe_fees( market_index: Serum3MarketIndex, coin_bank: &mut Bank, pc_bank: &mut Bank, - account: &mut MangoAccount, + account: &mut MangoAccountRefMut, after_oo: &OpenOrdersSlim, ) -> Result<()> { - let serum3_account = account.serum3.find_mut(market_index).unwrap(); + let serum3_account = account.serum3_find_mut(market_index).unwrap(); let maybe_actualized_coin_loan = I80F48::from_num::( serum3_account @@ -185,9 +184,10 @@ pub fn charge_maybe_fees( if maybe_actualized_coin_loan > 0 { serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved(); + drop(serum3_account); // loan origination fees - let coin_token_account = account.tokens.get_mut(coin_bank.token_index)?.0; + let coin_token_account = account.token_get_mut(coin_bank.token_index)?.0; let coin_token_native = coin_token_account.native(&coin_bank); if coin_token_native.is_negative() { @@ -201,6 +201,7 @@ pub fn charge_maybe_fees( } } + let serum3_account = account.serum3_find_mut(market_index).unwrap(); let maybe_actualized_pc_loan = I80F48::from_num::( serum3_account .previous_native_pc_reserved @@ -211,7 +212,7 @@ pub fn charge_maybe_fees( serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved(); // loan origination fees - let pc_token_account = account.tokens.get_mut(pc_bank.token_index)?.0; + let pc_token_account = account.token_get_mut(pc_bank.token_index)?.0; let pc_token_native = pc_token_account.native(&pc_bank); if pc_token_native.is_negative() { diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index b37645360..f35dceae5 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -14,11 +14,8 @@ use crate::logs::{DepositLog, TokenBalanceLog}; pub struct TokenDeposit<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, #[account( mut, @@ -61,10 +58,10 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // Get the account's position for that token index let mut account = ctx.accounts.account.load_mut()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let (position, raw_token_index, active_token_index) = - account.tokens.get_mut_or_create(token_index)?; + account.token_get_mut_or_create(token_index)?; let amount_i80f48 = I80F48::from(amount); let position_is_active = { @@ -77,16 +74,17 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { let indexed_position = position.indexed_position; - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?; + let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; let (bank, oracle_price) = retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?; // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) - account.net_deposits += cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::(); + account.fixed.net_deposits += + cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::(); emit!(TokenBalanceLog { mango_account: ctx.accounts.account.key(), - token_index: token_index, + token_index, indexed_position: indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), @@ -98,7 +96,7 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // TODO: This will be used to disable is_bankrupt or being_liquidated // when health recovers sufficiently // - let health = compute_health(&account, HealthType::Init, &retriever) + let health = compute_health(&account.borrow(), HealthType::Init, &retriever) .context("post-deposit init health")?; msg!("health: {}", health); @@ -109,7 +107,7 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // Deposits can deactivate a position if they cancel out a previous borrow. // if !position_is_active { - account.tokens.deactivate(raw_token_index); + account.token_deactivate(raw_token_index); } emit!(DepositLog { diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index a02bf6a2a..47d989a13 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -14,13 +14,8 @@ use crate::util::checked_math as cm; pub struct TokenWithdraw<'info> { pub group: AccountLoader<'info, Group>, - #[account( - mut, - has_one = group, - // note: should never be the delegate - has_one = owner, - )] - pub account: AccountLoader<'info, MangoAccount>, + #[account(mut, has_one = group, has_one = owner)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account( @@ -65,10 +60,10 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // Get the account's position for that token index let mut account = ctx.accounts.account.load_mut()?; - require!(!account.is_bankrupt(), MangoError::IsBankrupt); + require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt); let (position, raw_token_index, active_token_index) = - account.tokens.get_mut_or_create(token_index)?; + account.token_get_mut_or_create(token_index)?; // The bank will also be passed in remainingAccounts. Use an explicit scope // to drop the &mut before we borrow it immutably again later. @@ -121,16 +116,17 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let indexed_position = position.indexed_position; - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?; + let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; let (bank, oracle_price) = retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?; // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) - account.net_deposits -= cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::(); + account.fixed.net_deposits -= + cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::(); emit!(TokenBalanceLog { mango_account: ctx.accounts.account.key(), - token_index: token_index, + token_index, indexed_position: indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), @@ -140,7 +136,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // // Health check // - let health = compute_health(&account, HealthType::Init, &retriever) + let health = compute_health(&account.borrow(), HealthType::Init, &retriever) .context("post-withdraw init health")?; msg!("health: {}", health); require!(health >= 0, MangoError::HealthMustBePositive); @@ -151,13 +147,13 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // deactivated. // if !position_is_active { - account.tokens.deactivate(raw_token_index); + account.token_deactivate(raw_token_index); } emit!(WithdrawLog { mango_account: ctx.accounts.account.key(), signer: ctx.accounts.owner.key(), - token_index: token_index, + token_index, quantity: amount, price: oracle_price.to_bits(), }); diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index fd8da39fc..6a1f8ae86 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -15,18 +15,20 @@ pub mod error; pub mod events; pub mod instructions; pub mod logs; -mod serum3_cpi; +pub mod serum3_cpi; pub mod state; pub mod types; -use state::{OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex}; +use state::{ + AccountSize, OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex, +}; declare_id!("m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD"); #[program] pub mod mango_v4 { - use crate::state::OracleConfig; + use crate::state::{AccountSize, OracleConfig}; use super::*; @@ -125,9 +127,14 @@ pub mod mango_v4 { pub fn account_create( ctx: Context, account_num: u8, + account_size: AccountSize, name: String, ) -> Result<()> { - instructions::account_create(ctx, account_num, name) + instructions::account_create(ctx, account_num, account_size, name) + } + + pub fn account_expand(ctx: Context) -> Result<()> { + instructions::account_expand(ctx) } pub fn account_edit( diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 3762cfc90..04b4f7ae8 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -11,8 +11,8 @@ pub fn emit_perp_balances( pm: &PerpMarket, ) { emit!(PerpBalanceLog { - mango_account: mango_account, - market_index: market_index, + mango_account, + market_index, base_position: pp.base_position_lots, quote_position: pp.quote_position_native.to_bits(), long_settled_funding: pp.long_settled_funding.to_bits(), diff --git a/programs/mango-v4/src/state/dynamic_account.rs b/programs/mango-v4/src/state/dynamic_account.rs new file mode 100644 index 000000000..94fa3d0f5 --- /dev/null +++ b/programs/mango-v4/src/state/dynamic_account.rs @@ -0,0 +1,313 @@ +use std::cell::{Ref, RefMut}; + +use std::marker::PhantomData; +use std::mem::size_of; + +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use arrayref::array_ref; + +// Header is created by scanning and parsing dynamic portion of the account +// Header stores useful information e.g. offsets to easily seek into dynamic content +pub trait DynamicHeader: Sized { + // build header by scanning and parsing dynamic portion of the account + fn from_bytes(data: &[u8]) -> Result; + + // initialize a header on a new account, if necessary + fn initialize(data: &mut [u8]) -> Result<()>; +} + +pub trait DynamicAccountType: Owner + Discriminator { + type Header: DynamicHeader; + type Fixed: bytemuck::Pod; +} + +#[derive(Clone)] +pub struct DynamicAccount { + pub header: Header, + pub fixed: Fixed, + pub dynamic: Dynamic, +} + +pub type DynamicAccountValue = + DynamicAccount<::Header, ::Fixed, Vec>; +pub type DynamicAccountRef<'a, D> = DynamicAccount< + &'a ::Header, + &'a ::Fixed, + &'a [u8], +>; +pub type DynamicAccountRefMut<'a, D> = DynamicAccount< + &'a mut ::Header, + &'a mut ::Fixed, + &'a mut [u8], +>; + +// Want to generalize over: +// - T (which is Borrow) +// - &T (which is Borrow and Deref) +// - Ref (which is Deref) +pub trait DerefOrBorrow { + fn deref_or_borrow(&self) -> &T; +} + +impl DerefOrBorrow for T { + fn deref_or_borrow(&self) -> &T { + self + } +} + +impl DerefOrBorrow for &T { + fn deref_or_borrow(&self) -> &T { + self + } +} + +impl DerefOrBorrow<[T]> for Vec { + fn deref_or_borrow(&self) -> &[T] { + &self + } +} + +impl DerefOrBorrow for &mut T { + fn deref_or_borrow(&self) -> &T { + self + } +} + +impl<'a, T: ?Sized> DerefOrBorrow for Ref<'a, T> { + fn deref_or_borrow(&self) -> &T { + &self + } +} + +impl<'a, T: ?Sized> DerefOrBorrow for RefMut<'a, T> { + fn deref_or_borrow(&self) -> &T { + &self + } +} + +pub trait DerefOrBorrowMut { + fn deref_or_borrow_mut(&mut self) -> &mut T; +} + +impl DerefOrBorrowMut for T { + fn deref_or_borrow_mut(&mut self) -> &mut T { + self + } +} + +impl DerefOrBorrowMut for &mut T { + fn deref_or_borrow_mut(&mut self) -> &mut T { + self + } +} + +impl<'a, T: ?Sized> DerefOrBorrowMut for RefMut<'a, T> { + fn deref_or_borrow_mut(&mut self) -> &mut T { + self + } +} + +impl DerefOrBorrowMut<[T]> for Vec { + fn deref_or_borrow_mut(&mut self) -> &mut [T] { + self + } +} + +pub struct AccountLoaderDynamic<'info, D: DynamicAccountType> { + acc_info: AccountInfo<'info>, + phantom1: PhantomData<&'info D>, +} + +impl<'info, D: DynamicAccountType> AccountLoaderDynamic<'info, D> { + pub fn try_from(acc_info: &AccountInfo<'info>) -> Result { + if acc_info.owner != &D::owner() { + return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) + .with_pubkeys((*acc_info.owner, D::owner()))); + } + + let data = acc_info.try_borrow_data()?; + if data.len() < D::discriminator().len() { + return Err(ErrorCode::AccountDiscriminatorNotFound.into()); + } + let disc_bytes = array_ref![data, 0, 8]; + if disc_bytes != &D::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + + Ok(Self { + acc_info: acc_info.clone(), + phantom1: PhantomData, + }) + } + + pub fn try_from_unchecked(acc_info: &AccountInfo<'info>) -> Result { + if acc_info.owner != &D::owner() { + return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) + .with_pubkeys((*acc_info.owner, D::owner()))); + } + Ok(Self { + acc_info: acc_info.clone(), + phantom1: PhantomData, + }) + } + + /// Returns a Ref to the account data structure for reading. + pub fn load_fixed<'a>(&'a self) -> Result> { + let data = self.acc_info.try_borrow_data()?; + let fixed = Ref::map(data, |d| { + bytemuck::from_bytes(&d[8..8 + size_of::()]) + }); + Ok(fixed) + } + + /// Returns a Ref to the account data structure for reading. + pub fn load<'a>( + &'a self, + ) -> Result, Ref<'a, [u8]>>> { + let data = self.acc_info.try_borrow_data()?; + let header = D::Header::from_bytes(&data[8 + size_of::()..])?; + let (_, data) = Ref::map_split(data, |d| d.split_at(8)); + let (fixed_bytes, dynamic) = Ref::map_split(data, |d| d.split_at(size_of::())); + Ok(DynamicAccount { + header, + fixed: Ref::map(fixed_bytes, |b| bytemuck::from_bytes(b)), + dynamic, + }) + } + + pub fn load_init<'a>( + &'a self, + ) -> Result, RefMut<'a, [u8]>>> { + if !self.acc_info.is_writable { + return Err(ErrorCode::AccountNotMutable.into()); + } + + let mut data = self.acc_info.try_borrow_mut_data()?; + let mut disc_bytes = [0u8; 8]; + disc_bytes.copy_from_slice(&data[..8]); + let discriminator = u64::from_le_bytes(disc_bytes); + if discriminator != 0 { + return Err(ErrorCode::AccountDiscriminatorAlreadySet.into()); + } + + let disc_bytes: &mut [u8] = &mut data[0..8]; + disc_bytes.copy_from_slice(bytemuck::bytes_of(&(D::discriminator()))); + + D::Header::initialize(&mut data[8 + size_of::()..])?; + + drop(data); + + self.load_mut() + } + + /// Returns a Ref to the account data structure for reading. + pub fn load_mut<'a>( + &'a self, + ) -> Result, RefMut<'a, [u8]>>> { + if !self.acc_info.is_writable { + return Err(ErrorCode::AccountNotMutable.into()); + } + + let data = self.acc_info.try_borrow_mut_data()?; + let header = D::Header::from_bytes(&data[8 + size_of::()..])?; + let (_, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); + let (fixed_bytes, dynamic) = + RefMut::map_split(data, |d| d.split_at_mut(size_of::())); + Ok(DynamicAccount { + header, + fixed: RefMut::map(fixed_bytes, |b| bytemuck::from_bytes_mut(b)), + dynamic, + }) + } +} + +impl<'info, D: DynamicAccountType> anchor_lang::Accounts<'info> for AccountLoaderDynamic<'info, D> { + #[inline(never)] + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + _ix_data: &[u8], + _bumps: &mut std::collections::BTreeMap, + _reallocs: &mut std::collections::BTreeSet, + ) -> Result { + if accounts.is_empty() { + return Err(ErrorCode::AccountNotEnoughKeys.into()); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + let l = AccountLoaderDynamic::try_from(account)?; + Ok(l) + } +} + +impl<'info, D: DynamicAccountType> anchor_lang::AccountsExit<'info> + for AccountLoaderDynamic<'info, D> +{ + fn exit(&self, _program_id: &Pubkey) -> Result<()> { + // Normally anchor writes the discriminator again here, but I don't see why + let data = self.acc_info.try_borrow_data()?; + if data.len() < D::discriminator().len() { + return Err(ErrorCode::AccountDiscriminatorNotFound.into()); + } + let disc_bytes = array_ref![data, 0, 8]; + if disc_bytes != &D::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + Ok(()) + } +} + +impl<'info, D: DynamicAccountType> anchor_lang::AccountsClose<'info> + for AccountLoaderDynamic<'info, D> +{ + fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> { + close(self.to_account_info(), sol_destination) + } +} + +impl<'info, D: DynamicAccountType> anchor_lang::ToAccountMetas for AccountLoaderDynamic<'info, D> { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let is_signer = is_signer.unwrap_or(self.acc_info.is_signer); + let meta = match self.acc_info.is_writable { + false => AccountMeta::new_readonly(*self.acc_info.key, is_signer), + true => AccountMeta::new(*self.acc_info.key, is_signer), + }; + vec![meta] + } +} + +impl<'info, D: DynamicAccountType> AsRef> for AccountLoaderDynamic<'info, D> { + fn as_ref(&self) -> &AccountInfo<'info> { + &self.acc_info + } +} + +impl<'info, D: DynamicAccountType> anchor_lang::ToAccountInfos<'info> + for AccountLoaderDynamic<'info, D> +{ + fn to_account_infos(&self) -> Vec> { + vec![self.acc_info.clone()] + } +} + +impl<'info, D: DynamicAccountType> anchor_lang::Key for AccountLoaderDynamic<'info, D> { + fn key(&self) -> Pubkey { + *self.acc_info.key + } +} + +// https://github.com/coral-xyz/anchor/blob/master/lang/src/common.rs#L8 +fn close<'info>(info: AccountInfo<'info>, sol_destination: AccountInfo<'info>) -> Result<()> { + // Transfer tokens from the account to the sol_destination. + let dest_starting_lamports = sol_destination.lamports(); + **sol_destination.lamports.borrow_mut() = + dest_starting_lamports.checked_add(info.lamports()).unwrap(); + **info.lamports.borrow_mut() = 0; + // Mark the account discriminator as closed. + let mut data = info.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + dst[0..8].copy_from_slice(&[255, 255, 255, 255, 255, 255, 255, 255]); + + Ok(()) +} diff --git a/programs/mango-v4/src/state/equity.rs b/programs/mango-v4/src/state/equity.rs index c92802176..85a8639ff 100644 --- a/programs/mango-v4/src/state/equity.rs +++ b/programs/mango-v4/src/state/equity.rs @@ -6,16 +6,16 @@ use fixed::types::I80F48; use crate::events::{Equity, TokenEquity}; -use super::{MangoAccount, ScanningAccountRetriever}; +use super::{MangoAccountRef, ScanningAccountRetriever}; pub fn compute_equity( - account: &MangoAccount, + account: &MangoAccountRef, retriever: &ScanningAccountRetriever, ) -> Result { let mut token_equity_map = HashMap::new(); // token contributions - for (_i, position) in account.tokens.iter_active().enumerate() { + for (_i, position) in account.token_iter_active().enumerate() { let (bank, oracle_price) = retriever.scanned_bank_and_oracle(position.token_index)?; // converts the token value to the basis token value for health computations // TODO: health basis token == USDC? @@ -24,7 +24,7 @@ pub fn compute_equity( } // token contributions from Serum3 - for (_i, serum_account) in account.serum3.iter_active().enumerate() { + for (_i, serum_account) in account.serum3_iter_active().enumerate() { let oo = retriever.scanned_serum_oo(&serum_account.open_orders)?; // note base token value diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 16b7dd6c6..3d510fd3f 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -11,9 +11,11 @@ use std::collections::HashMap; use crate::accounts_zerocopy::*; use crate::error::*; use crate::serum3_cpi; -use crate::state::{oracle_price, Bank, MangoAccount, PerpMarket, PerpMarketIndex, TokenIndex}; +use crate::state::{oracle_price, Bank, PerpMarket, PerpMarketIndex, TokenIndex}; use crate::util::checked_math as cm; +use super::MangoAccountRef; + const BANKRUPTCY_DUST_THRESHOLD: I80F48 = I80F48!(0.000001); /// This trait abstracts how to find accounts needed for the health computation. @@ -59,11 +61,11 @@ pub struct FixedOrderAccountRetriever { pub fn new_fixed_order_account_retriever<'a, 'info>( ais: &'a [AccountInfo<'info>], - account: &MangoAccount, + account: &MangoAccountRef, ) -> Result>> { - let active_token_len = account.tokens.iter_active().count(); - let active_serum3_len = account.serum3.iter_active().count(); - let active_perp_len = account.perps.iter_active_accounts().count(); + let active_token_len = account.token_iter_active().count(); + let active_serum3_len = account.serum3_iter_active().count(); + let active_perp_len = account.perp_iter_active_accounts().count(); let expected_ais = cm!(active_token_len * 2 // banks + oracles + active_perp_len // PerpMarkets + active_serum3_len); // open_orders @@ -377,13 +379,13 @@ pub enum HealthType { /// /// These account infos must fit the fixed layout defined by FixedOrderAccountRetriever. pub fn compute_health_from_fixed_accounts( - account: &MangoAccount, + account: &MangoAccountRef, health_type: HealthType, ais: &[AccountInfo], ) -> Result { - let active_token_len = account.tokens.iter_active().count(); - let active_serum3_len = account.serum3.iter_active().count(); - let active_perp_len = account.perps.iter_active_accounts().count(); + let active_token_len = account.token_iter_active().count(); + let active_serum3_len = account.serum3_iter_active().count(); + let active_perp_len = account.perp_iter_active_accounts().count(); let expected_ais = cm!(active_token_len * 2 // banks + oracles + active_perp_len // PerpMarkets + active_serum3_len); // open_orders @@ -403,7 +405,7 @@ pub fn compute_health_from_fixed_accounts( /// Compute health with an arbitrary AccountRetriever pub fn compute_health( - account: &MangoAccount, + account: &MangoAccountRef, health_type: HealthType, retriever: &impl AccountRetriever, ) -> Result { @@ -785,15 +787,15 @@ fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result /// Generate a HealthCache for an account and its health accounts. pub fn new_health_cache( - account: &MangoAccount, + account: &MangoAccountRef, retriever: &impl AccountRetriever, ) -> Result { // token contribution from token accounts let mut token_infos = vec![]; - for (i, position) in account.tokens.iter_active().enumerate() { + for (i, position) in account.token_iter_active().enumerate() { let (bank, oracle_price) = - retriever.bank_and_oracle(&account.group, i, position.token_index)?; + retriever.bank_and_oracle(&account.fixed.group, i, position.token_index)?; // converts the token value to the basis token value for health computations // TODO: health basis token == USDC? @@ -814,7 +816,7 @@ pub fn new_health_cache( // Fill the TokenInfo balance with free funds in serum3 oo accounts, and fill // the serum3_max_reserved with their reserved funds. Also build Serum3Infos. let mut serum3_infos = vec![]; - for (i, serum_account) in account.serum3.iter_active().enumerate() { + for (i, serum_account) in account.serum3_iter_active().enumerate() { let oo = retriever.serum_oo(i, &serum_account.open_orders)?; // find the TokenInfos for the market's base and quote tokens @@ -851,9 +853,10 @@ pub fn new_health_cache( // TODO: also account for perp funding in health // health contribution from perp accounts - let mut perp_infos = Vec::with_capacity(account.perps.iter_active_accounts().count()); - for (i, perp_account) in account.perps.iter_active_accounts().enumerate() { - let perp_market = retriever.perp_market(&account.group, i, perp_account.market_index)?; + let mut perp_infos = Vec::with_capacity(account.perp_iter_active_accounts().count()); + for (i, perp_account) in account.perp_iter_active_accounts().enumerate() { + let perp_market = + retriever.perp_market(&account.fixed.group, i, perp_account.market_index)?; // find the TokenInfos for the market's base and quote tokens let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?; @@ -947,6 +950,7 @@ pub fn new_health_cache( mod tests { use super::*; use crate::state::oracle::StubOracle; + use crate::state::{MangoAccount, MangoAccountValue}; use std::cell::RefCell; use std::convert::identity; use std::mem::size_of; @@ -1065,7 +1069,9 @@ mod tests { // Run a health test that includes all the side values (like referrer_rebates_accrued) #[test] fn test_health0() { - let mut account = MangoAccount::default(); + let buffer = MangoAccount::default().try_to_vec().unwrap(); + let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); + let group = Pubkey::new_unique(); let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1); @@ -1073,20 +1079,20 @@ mod tests { bank1 .data() .deposit( - account.tokens.get_mut_or_create(1).unwrap().0, + account.token_get_mut_or_create(1).unwrap().0, I80F48::from(100), ) .unwrap(); bank2 .data() .withdraw_without_fee( - account.tokens.get_mut_or_create(4).unwrap().0, + account.token_get_mut_or_create(4).unwrap().0, I80F48::from(10), ) .unwrap(); let mut oo1 = TestAccount::::new_zeroed(); - let serum3account = account.serum3.create(2).unwrap(); + let serum3account = account.serum3_create(2).unwrap(); serum3account.open_orders = oo1.pubkey; serum3account.base_token_index = 4; serum3account.quote_token_index = 1; @@ -1106,7 +1112,7 @@ mod tests { perp1.data().maint_liab_weight = I80F48::from_num(1.0 + 0.1f64); perp1.data().quote_lot_size = 100; perp1.data().base_lot_size = 10; - let perpaccount = account.perps.get_account_mut_or_create(9).unwrap().0; + let perpaccount = account.perp_get_account_mut_or_create(9).unwrap().0; perpaccount.base_position_lots = 3; perpaccount.quote_position_native = -I80F48::from(310u16); perpaccount.bids_base_lots = 7; @@ -1142,7 +1148,7 @@ mod tests { let health3 = (3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + (-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0); assert!(health_eq( - compute_health(&account, HealthType::Init, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(), health1 + health2 + health3 )); } @@ -1226,7 +1232,9 @@ mod tests { expected_health: f64, } fn test_health1_runner(testcase: &TestHealth1Case) { - let mut account = MangoAccount::default(); + let buffer = MangoAccount::default().try_to_vec().unwrap(); + let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); + let group = Pubkey::new_unique(); let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1); @@ -1235,27 +1243,27 @@ mod tests { bank1 .data() .change_without_fee( - account.tokens.get_mut_or_create(1).unwrap().0, + account.token_get_mut_or_create(1).unwrap().0, I80F48::from(testcase.token1), ) .unwrap(); bank2 .data() .change_without_fee( - account.tokens.get_mut_or_create(4).unwrap().0, + account.token_get_mut_or_create(4).unwrap().0, I80F48::from(testcase.token2), ) .unwrap(); bank3 .data() .change_without_fee( - account.tokens.get_mut_or_create(5).unwrap().0, + account.token_get_mut_or_create(5).unwrap().0, I80F48::from(testcase.token3), ) .unwrap(); let mut oo1 = TestAccount::::new_zeroed(); - let serum3account1 = account.serum3.create(2).unwrap(); + let serum3account1 = account.serum3_create(2).unwrap(); serum3account1.open_orders = oo1.pubkey; serum3account1.base_token_index = 4; serum3account1.quote_token_index = 1; @@ -1263,7 +1271,7 @@ mod tests { oo1.data().native_coin_total = testcase.oo_1_2.1; let mut oo2 = TestAccount::::new_zeroed(); - let serum3account2 = account.serum3.create(3).unwrap(); + let serum3account2 = account.serum3_create(3).unwrap(); serum3account2.open_orders = oo2.pubkey; serum3account2.base_token_index = 5; serum3account2.quote_token_index = 1; @@ -1280,7 +1288,7 @@ mod tests { perp1.data().maint_liab_weight = I80F48::from_num(1.0 + 0.1f64); perp1.data().quote_lot_size = 100; perp1.data().base_lot_size = 10; - let perpaccount = account.perps.get_account_mut_or_create(9).unwrap().0; + let perpaccount = account.perp_get_account_mut_or_create(9).unwrap().0; perpaccount.base_position_lots = testcase.perp1.0; perpaccount.quote_position_native = I80F48::from(testcase.perp1.1); perpaccount.bids_base_lots = testcase.perp1.2; @@ -1310,7 +1318,7 @@ mod tests { }; assert!(health_eq( - compute_health(&account, HealthType::Init, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(), testcase.expected_health )); } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 34b181e30..4cba2e328 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1,714 +1,81 @@ -use anchor_lang::prelude::*; -use checked_math as cm; -use fixed::types::I80F48; -use static_assertions::const_assert_eq; -use std::cmp::Ordering; +use std::fmt; + use std::mem::size_of; -use crate::error::*; -use crate::state::*; +use anchor_lang::prelude::*; +use arrayref::array_ref; -// todo: these are arbitrary -// ckamm: Either we put hard limits on everything, or we have a simple model for how much -// compute a token/serum/perp market needs, so users who don't use serum markets can have -// more perp markets open at the same time etc -// In particular if perp markets don't require the base token to be active on the account, -// we could probably support 1 token (quote currency) + 15 active perp markets at the same time -// It's a tradeoff between allowing users to trade on many markets with one account, -// MangoAccount size and health compute needs. -const MAX_TOKEN_POSITIONS: usize = 16; -const MAX_SERUM3_ACCOUNTS: usize = 8; -const MAX_PERP_ACCOUNTS: usize = 8; -pub const MAX_PERP_OPEN_ORDERS: usize = 8; +use fixed::types::I80F48; +use num_enum::IntoPrimitive; +use num_enum::TryFromPrimitive; +use solana_program::program_memory::sol_memmove; +use static_assertions::const_assert_eq; -pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX; +use crate::error::Contextable; +use crate::error::MangoError; +use crate::error_msg; -#[zero_copy] -#[derive(Debug)] -pub struct TokenPosition { - // TODO: Why did we have deposits and borrows as two different values - // if only one of them was allowed to be != 0 at a time? - // todo: maybe we want to split collateral and lending? - // todo: see https://github.com/blockworks-foundation/mango-v4/issues/1 - // todo: how does ftx do this? - /// The deposit_index (if positive) or borrow_index (if negative) scaled position - pub indexed_position: I80F48, +use super::dynamic_account::*; +use super::FillEvent; +use super::LeafNode; +use super::PerpMarket; +use super::PerpMarketIndex; +use super::PerpOpenOrders; +use super::Serum3MarketIndex; +use super::Side; +use super::TokenIndex; +use super::FREE_ORDER_SLOT; +use super::{PerpPositions, Serum3Orders, TokenPosition}; +use checked_math as cm; - /// index into Group.tokens - pub token_index: TokenIndex, +type BorshVecLength = u32; +const BORSH_VEC_PADDING_BYTES: usize = 4; +const BORSH_VEC_SIZE_BYTES: usize = 4; - /// incremented when a market requires this position to stay alive - pub in_use_count: u8, +#[derive( + Debug, + Eq, + PartialEq, + Clone, + Copy, + TryFromPrimitive, + IntoPrimitive, + AnchorSerialize, + AnchorDeserialize, +)] +#[repr(u8)] - pub reserved: [u8; 5], -} -const_assert_eq!(size_of::(), 24); -const_assert_eq!(size_of::() % 8, 0); - -impl TokenPosition { - pub fn is_active(&self) -> bool { - self.token_index != TokenIndex::MAX - } - - pub fn is_active_for_token(&self, token_index: TokenIndex) -> bool { - self.token_index == token_index - } - - pub fn native(&self, bank: &Bank) -> I80F48 { - if self.indexed_position.is_positive() { - self.indexed_position * bank.deposit_index - } else { - self.indexed_position * bank.borrow_index - } - } - - pub fn ui(&self, bank: &Bank) -> I80F48 { - if self.indexed_position.is_positive() { - (self.indexed_position * bank.deposit_index) - / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) - } else { - (self.indexed_position * bank.borrow_index) - / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) - } - } - - pub fn is_in_use(&self) -> bool { - self.in_use_count > 0 - } +pub enum AccountSize { + Small = 0, + Large = 1, } -#[zero_copy] -pub struct MangoAccountTokenPositions { - pub values: [TokenPosition; MAX_TOKEN_POSITIONS], -} -const_assert_eq!( - size_of::(), - MAX_TOKEN_POSITIONS * size_of::() -); -const_assert_eq!(size_of::() % 8, 0); - -impl std::fmt::Debug for MangoAccountTokenPositions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MangoAccountTokens") - .field( - "values", - &self - .values - .iter() - .filter(|value| value.is_active()) - .collect::>(), - ) - .finish() - } -} - -impl Default for MangoAccountTokenPositions { - fn default() -> Self { - Self::new() - } -} - -impl MangoAccountTokenPositions { - pub fn new() -> Self { - Self { - values: [TokenPosition { - indexed_position: I80F48::ZERO, - token_index: TokenIndex::MAX, - in_use_count: 0, - reserved: Default::default(), - }; MAX_TOKEN_POSITIONS], - } - } - - /// Returns - /// - the position - /// - the raw index into the token positions list (for use with get_raw/deactivate) - pub fn get(&self, token_index: TokenIndex) -> Result<(&TokenPosition, usize)> { - self.values - .iter() - .enumerate() - .find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index))) - .ok_or_else(|| error_msg!("position for token index {} not found", token_index)) - } - - /// Returns - /// - the position - /// - the raw index into the token positions list (for use with get_raw/deactivate) - pub fn get_mut(&mut self, token_index: TokenIndex) -> Result<(&mut TokenPosition, usize)> { - self.values - .iter_mut() - .enumerate() - .find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index))) - .ok_or_else(|| error_msg!("position for token index {} not found", token_index)) - } - - pub fn get_mut_raw(&mut self, raw_token_index: usize) -> &mut TokenPosition { - &mut self.values[raw_token_index] - } - - pub fn get_raw(&self, raw_token_index: usize) -> &TokenPosition { - &self.values[raw_token_index] - } - - /// Creates or retrieves a TokenPosition for the token_index. - /// Returns: - /// - the position - /// - the raw index into the token positions list (for use with get_raw) - /// - the active index, for use with FixedOrderAccountRetriever - pub fn get_mut_or_create( - &mut self, - token_index: TokenIndex, - ) -> Result<(&mut TokenPosition, usize, usize)> { - let mut active_index = 0; - let mut match_or_free = None; - for (raw_index, position) in self.values.iter().enumerate() { - if position.is_active_for_token(token_index) { - // Can't return early because of lifetimes - match_or_free = Some((raw_index, active_index)); - break; - } - if position.is_active() { - active_index += 1; - } else if match_or_free.is_none() { - match_or_free = Some((raw_index, active_index)); - } - } - if let Some((raw_index, bank_index)) = match_or_free { - let v = &mut self.values[raw_index]; - if !v.is_active_for_token(token_index) { - *v = TokenPosition { - indexed_position: I80F48::ZERO, - token_index, - in_use_count: 0, - reserved: Default::default(), - }; - } - Ok((v, raw_index, bank_index)) - } else { - err!(MangoError::NoFreeTokenPositionIndex) - .context(format!("when looking for token index {}", token_index)) - } - } - - pub fn deactivate(&mut self, index: usize) { - assert!(self.values[index].in_use_count == 0); - self.values[index].token_index = TokenIndex::MAX; - } - - pub fn iter_active(&self) -> impl Iterator { - self.values.iter().filter(|p| p.is_active()) - } - - pub fn find(&self, token_index: TokenIndex) -> Option<&TokenPosition> { - self.values - .iter() - .find(|p| p.is_active_for_token(token_index)) - } -} - -#[zero_copy] -#[derive(Debug)] -pub struct Serum3Orders { - pub open_orders: Pubkey, - - // tracks reserved funds in open orders account, - // used for bookkeeping of potentital loans which - // can be charged with loan origination fees - // e.g. serum3 settle funds ix - pub previous_native_coin_reserved: u64, - pub previous_native_pc_reserved: u64, - - pub market_index: Serum3MarketIndex, - - /// Store the base/quote token index, so health computations don't need - /// to get passed the static SerumMarket to find which tokens a market - /// uses and look up the correct oracles. - pub base_token_index: TokenIndex, - pub quote_token_index: TokenIndex, - - pub reserved: [u8; 2], -} -const_assert_eq!(size_of::(), 32 + 8 * 2 + 2 * 3 + 2); -const_assert_eq!(size_of::() % 8, 0); - -impl Serum3Orders { - pub fn is_active(&self) -> bool { - self.market_index != Serum3MarketIndex::MAX - } - - pub fn is_active_for_market(&self, market_index: Serum3MarketIndex) -> bool { - self.market_index == market_index - } -} - -impl Default for Serum3Orders { - fn default() -> Self { - Self { - open_orders: Pubkey::default(), - market_index: Serum3MarketIndex::MAX, - base_token_index: TokenIndex::MAX, - quote_token_index: TokenIndex::MAX, - reserved: Default::default(), - previous_native_coin_reserved: 0, - previous_native_pc_reserved: 0, +impl fmt::Display for AccountSize { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + AccountSize::Small => write!(f, "Small"), + AccountSize::Large => write!(f, "Large"), } } } -#[zero_copy] -pub struct MangoAccountSerum3Orders { - pub values: [Serum3Orders; MAX_SERUM3_ACCOUNTS], -} -const_assert_eq!( - size_of::(), - MAX_SERUM3_ACCOUNTS * size_of::() -); -const_assert_eq!(size_of::() % 8, 0); - -impl std::fmt::Debug for MangoAccountSerum3Orders { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MangoAccountSerum3") - .field( - "values", - &self - .values - .iter() - .filter(|value| value.is_active()) - .collect::>(), - ) - .finish() - } -} - -impl Default for MangoAccountSerum3Orders { - fn default() -> Self { - Self::new() - } -} - -impl MangoAccountSerum3Orders { - pub fn new() -> Self { - Self { - values: [Serum3Orders::default(); MAX_SERUM3_ACCOUNTS], - } - } - - pub fn create(&mut self, market_index: Serum3MarketIndex) -> Result<&mut Serum3Orders> { - if self.find(market_index).is_some() { - return err!(MangoError::Serum3OpenOrdersExistAlready); - } - if let Some(v) = self.values.iter_mut().find(|p| !p.is_active()) { - *v = Serum3Orders { - market_index: market_index as Serum3MarketIndex, - ..Serum3Orders::default() - }; - Ok(v) - } else { - err!(MangoError::NoFreeSerum3OpenOrdersIndex) - } - } - - pub fn deactivate(&mut self, market_index: Serum3MarketIndex) -> Result<()> { - let index = self - .values - .iter() - .position(|p| p.is_active_for_market(market_index)) - .ok_or_else(|| error_msg!("serum3 open orders index {} not found", market_index))?; - - self.values[index].market_index = Serum3MarketIndex::MAX; - - Ok(()) - } - - pub fn iter_active(&self) -> impl Iterator { - self.values.iter().filter(|p| p.is_active()) - } - - pub fn find(&self, market_index: Serum3MarketIndex) -> Option<&Serum3Orders> { - self.values - .iter() - .find(|p| p.is_active_for_market(market_index)) - } - - pub fn find_mut(&mut self, market_index: Serum3MarketIndex) -> Option<&mut Serum3Orders> { - self.values - .iter_mut() - .find(|p| p.is_active_for_market(market_index)) - } -} - -#[zero_copy] -pub struct PerpPositions { - pub market_index: PerpMarketIndex, - pub reserved: [u8; 6], - - /// Active position size, measured in base lots - pub base_position_lots: i64, - /// Active position in quote (conversation rate is that of the time the order was settled) - /// measured in native quote - pub quote_position_native: I80F48, - - /// Already settled funding - pub long_settled_funding: I80F48, - pub short_settled_funding: I80F48, - - /// Base lots in bids - pub bids_base_lots: i64, - /// Base lots in asks - pub asks_base_lots: i64, - - /// Liquidity mining rewards - // pub mngo_accrued: u64, - - /// Amount that's on EventQueue waiting to be processed - pub taker_base_lots: i64, - pub taker_quote_lots: i64, -} - -impl std::fmt::Debug for PerpPositions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PerpAccount") - .field("market_index", &self.market_index) - .field("base_position_lots", &self.base_position_lots) - .field("quote_position_native", &self.quote_position_native) - .field("bids_base_lots", &self.bids_base_lots) - .field("asks_base_lots", &self.asks_base_lots) - .field("taker_base_lots", &self.taker_base_lots) - .field("taker_quote_lots", &self.taker_quote_lots) - .finish() - } -} -const_assert_eq!(size_of::(), 8 + 8 * 5 + 3 * 16); -const_assert_eq!(size_of::() % 8, 0); - -impl Default for PerpPositions { - fn default() -> Self { - Self { - market_index: PerpMarketIndex::MAX, - base_position_lots: 0, - quote_position_native: I80F48::ZERO, - bids_base_lots: 0, - asks_base_lots: 0, - taker_base_lots: 0, - taker_quote_lots: 0, - reserved: Default::default(), - long_settled_funding: I80F48::ZERO, - short_settled_funding: I80F48::ZERO, +impl AccountSize { + pub fn space(&self) -> (u8, u8, u8, u8) { + match self { + AccountSize::Small => (8, 2, 2, 2), + AccountSize::Large => (16, 8, 8, 8), } } } -impl PerpPositions { - /// Add taker trade after it has been matched but before it has been process on EventQueue - pub fn add_taker_trade(&mut self, side: Side, base_lots: i64, quote_lots: i64) { - match side { - Side::Bid => { - self.taker_base_lots = cm!(self.taker_base_lots + base_lots); - self.taker_quote_lots = cm!(self.taker_quote_lots - quote_lots); - } - Side::Ask => { - self.taker_base_lots = cm!(self.taker_base_lots - base_lots); - self.taker_quote_lots = cm!(self.taker_quote_lots + quote_lots); - } - } - } - /// Remove taker trade after it has been processed on EventQueue - pub fn remove_taker_trade(&mut self, base_change: i64, quote_change: i64) { - self.taker_base_lots = cm!(self.taker_base_lots - base_change); - self.taker_quote_lots = cm!(self.taker_quote_lots - quote_change); - } - - pub fn is_active(&self) -> bool { - self.market_index != PerpMarketIndex::MAX - } - - pub fn is_active_for_market(&self, market_index: PerpMarketIndex) -> bool { - self.market_index == market_index - } - - /// This assumes settle_funding was already called - pub fn change_base_position(&mut self, perp_market: &mut PerpMarket, base_change: i64) { - let start = self.base_position_lots; - self.base_position_lots += base_change; - perp_market.open_interest += self.base_position_lots.abs() - start.abs(); - } - - /// Move unrealized funding payments into the quote_position - pub fn settle_funding(&mut self, perp_market: &PerpMarket) { - match self.base_position_lots.cmp(&0) { - Ordering::Greater => { - self.quote_position_native -= (perp_market.long_funding - - self.long_settled_funding) - * I80F48::from_num(self.base_position_lots); - } - Ordering::Less => { - self.quote_position_native -= (perp_market.short_funding - - self.short_settled_funding) - * I80F48::from_num(self.base_position_lots); - } - Ordering::Equal => (), - } - self.long_settled_funding = perp_market.long_funding; - self.short_settled_funding = perp_market.short_funding; - } -} - -#[zero_copy] -pub struct MangoAccountPerpPositions { - pub accounts: [PerpPositions; MAX_PERP_ACCOUNTS], - - // TODO: possibly it's more convenient to store a single list of PerpOpenOrder structs? - pub order_market: [PerpMarketIndex; MAX_PERP_OPEN_ORDERS], - pub order_side: [Side; MAX_PERP_OPEN_ORDERS], // TODO: storing enums isn't POD - pub order_id: [i128; MAX_PERP_OPEN_ORDERS], - pub client_order_id: [u64; MAX_PERP_OPEN_ORDERS], -} -const_assert_eq!( - size_of::(), - MAX_PERP_ACCOUNTS * size_of::() + MAX_PERP_OPEN_ORDERS * (2 + 1 + 16 + 8) -); -const_assert_eq!(size_of::() % 8, 0); - -impl std::fmt::Debug for MangoAccountPerpPositions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MangoAccountPerps") - .field( - "accounts", - &self - .accounts - .iter() - .filter(|value| value.is_active()) - .collect::>(), - ) - .field( - "order_market", - &self - .order_market - .iter() - .filter(|value| **value != PerpMarketIndex::MAX) - .collect::>(), - ) - .field( - "order_side", - &self - .order_side - .iter() - .zip(self.order_id) - .filter(|value| value.1 != 0) - .map(|value| value.0) - .collect::>(), - ) - .field( - "order_id", - &self - .order_id - .iter() - .filter(|value| **value != 0) - .collect::>(), - ) - .field( - "client_order_id", - &self - .client_order_id - .iter() - .filter(|value| **value != 0) - .collect::>(), - ) - .finish() - } -} - -impl MangoAccountPerpPositions { - pub fn new() -> Self { - Self { - accounts: [PerpPositions::default(); MAX_PERP_ACCOUNTS], - order_market: [FREE_ORDER_SLOT; MAX_PERP_OPEN_ORDERS], - order_side: [Side::Bid; MAX_PERP_OPEN_ORDERS], - order_id: [0; MAX_PERP_OPEN_ORDERS], - client_order_id: [0; MAX_PERP_OPEN_ORDERS], - } - } - - pub fn get_account_mut_or_create( - &mut self, - perp_market_index: PerpMarketIndex, - ) -> Result<(&mut PerpPositions, usize)> { - let mut pos = self - .accounts - .iter() - .position(|p| p.is_active_for_market(perp_market_index)); - if pos.is_none() { - pos = self.accounts.iter().position(|p| !p.is_active()); - if let Some(i) = pos { - self.accounts[i] = PerpPositions { - market_index: perp_market_index, - ..Default::default() - }; - } - } - if let Some(i) = pos { - Ok((&mut self.accounts[i], i)) - } else { - err!(MangoError::NoFreePerpPositionIndex) - } - } - - pub fn deactivate_account(&mut self, index: usize) { - self.accounts[index].market_index = PerpMarketIndex::MAX; - } - - pub fn iter_active_accounts(&self) -> impl Iterator { - self.accounts.iter().filter(|p| p.is_active()) - } - - pub fn find_account(&self, market_index: PerpMarketIndex) -> Option<&PerpPositions> { - self.accounts - .iter() - .find(|p| p.is_active_for_market(market_index)) - } - - pub fn next_order_slot(&self) -> Option { - self.order_market.iter().position(|&i| i == FREE_ORDER_SLOT) - } - - pub fn add_order( - &mut self, - perp_market_index: PerpMarketIndex, - side: Side, - order: &LeafNode, - ) -> Result<()> { - let mut perp_account = self.get_account_mut_or_create(perp_market_index).unwrap().0; - match side { - Side::Bid => { - perp_account.bids_base_lots = cm!(perp_account.bids_base_lots + order.quantity); - } - Side::Ask => { - perp_account.asks_base_lots = cm!(perp_account.asks_base_lots + order.quantity); - } - }; - let slot = order.owner_slot as usize; - self.order_market[slot] = perp_market_index; - self.order_side[slot] = side; - self.order_id[slot] = order.key; - self.client_order_id[slot] = order.client_order_id; - Ok(()) - } - - pub fn remove_order(&mut self, slot: usize, quantity: i64) -> Result<()> { - require_neq!(self.order_market[slot], FREE_ORDER_SLOT); - let order_side = self.order_side[slot]; - let perp_market_index = self.order_market[slot]; - let perp_account = self.get_account_mut_or_create(perp_market_index).unwrap().0; - - // accounting - match order_side { - Side::Bid => { - perp_account.bids_base_lots = cm!(perp_account.bids_base_lots - quantity); - } - Side::Ask => { - perp_account.asks_base_lots = cm!(perp_account.asks_base_lots - quantity); - } - } - - // release space - self.order_market[slot] = FREE_ORDER_SLOT; - - self.order_side[slot] = Side::Bid; - self.order_id[slot] = 0i128; - self.client_order_id[slot] = 0u64; - Ok(()) - } - - pub fn execute_maker( - &mut self, - perp_market_index: PerpMarketIndex, - perp_market: &mut PerpMarket, - fill: &FillEvent, - ) -> Result<()> { - let pa = self.get_account_mut_or_create(perp_market_index).unwrap().0; - pa.settle_funding(perp_market); - - let side = fill.taker_side.invert_side(); - let (base_change, quote_change) = fill.base_quote_change(side); - pa.change_base_position(perp_market, base_change); - let quote = I80F48::from_num( - perp_market - .quote_lot_size - .checked_mul(quote_change) - .unwrap(), - ); - let fees = quote.abs() * fill.maker_fee; - if !fill.market_fees_applied { - perp_market.fees_accrued += fees; - } - pa.quote_position_native = pa.quote_position_native.checked_add(quote - fees).unwrap(); - - if fill.maker_out { - self.remove_order(fill.maker_slot as usize, base_change.abs()) - } else { - match side { - Side::Bid => { - pa.bids_base_lots = cm!(pa.bids_base_lots - base_change.abs()); - } - Side::Ask => { - pa.asks_base_lots = cm!(pa.asks_base_lots - base_change.abs()); - } - } - Ok(()) - } - } - - pub fn execute_taker( - &mut self, - perp_market_index: PerpMarketIndex, - perp_market: &mut PerpMarket, - fill: &FillEvent, - ) -> Result<()> { - let pa = self.get_account_mut_or_create(perp_market_index).unwrap().0; - pa.settle_funding(perp_market); - - let (base_change, quote_change) = fill.base_quote_change(fill.taker_side); - pa.remove_taker_trade(base_change, quote_change); - pa.change_base_position(perp_market, base_change); - let quote = I80F48::from_num(perp_market.quote_lot_size * quote_change); - - // fees are assessed at time of trade; no need to assess fees here - - pa.quote_position_native += quote; - Ok(()) - } - - pub fn find_order_with_client_order_id( - &self, - market_index: PerpMarketIndex, - client_order_id: u64, - ) -> Option<(i128, Side)> { - for i in 0..MAX_PERP_OPEN_ORDERS { - if self.order_market[i] == market_index && self.client_order_id[i] == client_order_id { - return Some((self.order_id[i], self.order_side[i])); - } - } - None - } - - pub fn find_order_side(&self, market_index: PerpMarketIndex, order_id: i128) -> Option { - for i in 0..MAX_PERP_OPEN_ORDERS { - if self.order_market[i] == market_index && self.order_id[i] == order_id { - return Some(self.order_side[i]); - } - } - None - } -} - -impl Default for MangoAccountPerpPositions { - fn default() -> Self { - Self::new() - } -} - -#[account(zero_copy)] +// Mango Account +// This struct definition is only for clients e.g. typescript, so that they can easily use out of the box +// deserialization and not have to do custom deserialization +// On chain, we would prefer zero-copying to optimize for compute +#[account] pub struct MangoAccount { + // fixed + // note: keep MangoAccountFixed in sync with changes here // ABI: Clients rely on this being at offset 8 pub group: Pubkey, @@ -720,16 +87,6 @@ pub struct MangoAccount { // Alternative authority/signer of transactions for a mango account pub delegate: Pubkey, - // Maps token_index -> deposit/borrow account for each token - // that is active on this MangoAccount. - pub tokens: MangoAccountTokenPositions, - - // Maps serum_market_index -> open orders for each serum market - // that is active on this MangoAccount. - pub serum3: MangoAccountSerum3Orders, - - pub perps: MangoAccountPerpPositions, - /// This account cannot open new positions or borrow until `init_health >= 0` being_liquidated: u8, @@ -749,39 +106,125 @@ pub struct MangoAccount { // Cumulative settles on perp positions // TODO: unimplemented pub net_settled: f32, -} -const_assert_eq!( - size_of::(), - 32 + 3 * 32 - + size_of::() - + size_of::() - + size_of::() - + 4 - + 4 - + 2 * 4 // net_deposits and net_settled -); -const_assert_eq!(size_of::() % 8, 0); -impl std::fmt::Debug for MangoAccount { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MangoAccount") - .field("name", &self.name()) - .field("group", &self.group) - .field("owner", &self.owner) - .field("delegate", &self.delegate) - .field("tokens", &self.tokens) - .field("serum3", &self.serum3) - .field("perps", &self.perps) - .field("being_liquidated", &self.being_liquidated) - .field("is_bankrupt", &self.is_bankrupt) - .field("account_num", &self.account_num) - .field("bump", &self.bump) - .field("reserved", &self.reserved) - .finish() + // dynamic + // note: padding is required for TokenPosition, etc. to be aligned + pub padding1: u32, + // Maps token_index -> deposit/borrow account for each token + // that is active on this MangoAccount. + pub tokens: Vec, + pub padding2: u32, + // Maps serum_market_index -> open orders for each serum market + // that is active on this MangoAccount. + pub serum3: Vec, + pub padding3: u32, + pub perps: Vec, + pub padding4: u32, + pub perp_open_orders: Vec, +} + +impl Default for MangoAccount { + fn default() -> Self { + Self { + name: Default::default(), + group: Pubkey::default(), + owner: Pubkey::default(), + delegate: Pubkey::default(), + being_liquidated: 0, + is_bankrupt: 0, + account_num: 0, + bump: 0, + reserved: Default::default(), + net_deposits: 0.0, + net_settled: 0.0, + padding1: Default::default(), + tokens: vec![TokenPosition::default(); 3], + padding2: Default::default(), + serum3: vec![Serum3Orders::default(); 5], + padding3: Default::default(), + perps: vec![PerpPositions::default(); 2], + padding4: Default::default(), + perp_open_orders: vec![PerpOpenOrders::default(); 2], + } } } impl MangoAccount { + pub fn space(account_size: AccountSize) -> usize { + let (token_count, serum3_count, perp_count, perp_oo_count) = account_size.space(); + + 8 + size_of::() + + Self::dynamic_size(token_count, serum3_count, perp_count, perp_oo_count) + } + + pub fn dynamic_token_vec_offset() -> usize { + BORSH_VEC_PADDING_BYTES + } + + pub fn dynamic_serum3_vec_offset(token_count: u8) -> usize { + Self::dynamic_token_vec_offset() + + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(token_count)) + + BORSH_VEC_PADDING_BYTES + } + + pub fn dynamic_perp_vec_offset(token_count: u8, serum3_count: u8) -> usize { + Self::dynamic_serum3_vec_offset(token_count) + + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(serum3_count)) + + BORSH_VEC_PADDING_BYTES + } + + pub fn dynamic_perp_oo_vec_offset(token_count: u8, serum3_count: u8, perp_count: u8) -> usize { + Self::dynamic_perp_vec_offset(token_count, serum3_count) + + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(perp_count)) + + BORSH_VEC_PADDING_BYTES + } + + pub fn dynamic_size( + token_count: u8, + serum3_count: u8, + perp_count: u8, + perp_oo_count: u8, + ) -> usize { + Self::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count) + + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(perp_oo_count)) + } +} + +#[test] +fn test_dynamic_offsets() { + let mut account = MangoAccount::default(); + account.tokens.resize(16, TokenPosition::default()); + account.serum3.resize(8, Serum3Orders::default()); + account.perps.resize(8, PerpPositions::default()); + account + .perp_open_orders + .resize(8, PerpOpenOrders::default()); + assert_eq!( + 8 + AnchorSerialize::try_to_vec(&account).unwrap().len(), + MangoAccount::space(AccountSize::Large.try_into().unwrap()) + ); +} + +// Mango Account fixed part for easy zero copy deserialization +#[derive(Copy, Clone, bytemuck::Zeroable, bytemuck::Pod)] +#[repr(C)] +pub struct MangoAccountFixed { + pub group: Pubkey, + pub owner: Pubkey, + pub name: [u8; 32], + pub delegate: Pubkey, + being_liquidated: u8, + is_bankrupt: u8, + pub account_num: u8, + pub bump: u8, + pub reserved: [u8; 4], + pub net_deposits: f32, + pub net_settled: f32, +} +const_assert_eq!(size_of::(), 32 * 4 + 4 + 4 + 2 * 4); +const_assert_eq!(size_of::() % 8, 0); + +impl MangoAccountFixed { pub fn name(&self) -> &str { std::str::from_utf8(&self.name) .unwrap() @@ -809,38 +252,726 @@ impl MangoAccount { } } -impl Default for MangoAccount { - fn default() -> Self { - Self { - name: Default::default(), - group: Pubkey::default(), - owner: Pubkey::default(), - delegate: Pubkey::default(), - tokens: MangoAccountTokenPositions::new(), - serum3: MangoAccountSerum3Orders::new(), - perps: MangoAccountPerpPositions::new(), - being_liquidated: 0, - is_bankrupt: 0, - account_num: 0, - bump: 0, - reserved: Default::default(), - net_deposits: 0.0, - net_settled: 0.0, - } +impl DynamicAccountType for MangoAccount { + type Header = MangoAccountDynamicHeader; + type Fixed = MangoAccountFixed; +} + +#[derive(Clone)] +pub struct MangoAccountDynamicHeader { + pub token_count: u8, + pub serum3_count: u8, + pub perp_count: u8, + pub perp_oo_count: u8, +} + +impl DynamicHeader for MangoAccountDynamicHeader { + fn from_bytes(data: &[u8]) -> Result { + let token_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ + data, + MangoAccount::dynamic_token_vec_offset(), + BORSH_VEC_SIZE_BYTES + ])) + .unwrap(); + + let serum3_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ + data, + MangoAccount::dynamic_serum3_vec_offset(token_count), + BORSH_VEC_SIZE_BYTES + ])) + .unwrap(); + + let perp_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ + data, + MangoAccount::dynamic_perp_vec_offset(token_count, serum3_count), + BORSH_VEC_SIZE_BYTES + ])) + .unwrap(); + + let perp_oo_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![ + data, + MangoAccount::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count), + BORSH_VEC_SIZE_BYTES + ])) + .unwrap(); + + Ok(Self { + token_count, + serum3_count, + perp_count, + perp_oo_count, + }) + } + + fn initialize(_data: &mut [u8]) -> Result<()> { + Ok(()) } } -#[macro_export] -macro_rules! account_seeds { - ( $account:expr ) => { - &[ - $account.group.as_ref(), - b"account".as_ref(), - $account.owner.as_ref(), - &$account.account_num.to_le_bytes(), - &[$account.bump], - ] - }; +fn get_helper(data: &[u8], index: usize) -> &T { + bytemuck::from_bytes(&data[index..index + size_of::()]) } -pub use account_seeds; +fn get_helper_mut(data: &mut [u8], index: usize) -> &mut T { + bytemuck::from_bytes_mut(&mut data[index..index + size_of::()]) +} + +impl MangoAccountDynamicHeader { + // offset into dynamic data where 1st TokenPosition would be found + // todo make fn private + pub fn token_offset(&self, raw_index: usize) -> usize { + MangoAccount::dynamic_token_vec_offset() + + BORSH_VEC_SIZE_BYTES + + raw_index * size_of::() + } + + // offset into dynamic data where 1st Serum3Orders would be found + // todo make fn private + pub fn serum3_offset(&self, raw_index: usize) -> usize { + MangoAccount::dynamic_serum3_vec_offset(self.token_count) + + BORSH_VEC_SIZE_BYTES + + raw_index * size_of::() + } + + // offset into dynamic data where 1st PerpPositions would be found + fn perp_offset(&self, raw_index: usize) -> usize { + MangoAccount::dynamic_perp_vec_offset(self.token_count, self.serum3_count) + + BORSH_VEC_SIZE_BYTES + + raw_index * size_of::() + } + + fn perp_oo_offset(&self, raw_index: usize) -> usize { + MangoAccount::dynamic_perp_oo_vec_offset( + self.token_count, + self.serum3_count, + self.perp_count, + ) + BORSH_VEC_SIZE_BYTES + + raw_index * size_of::() + } + + pub fn token_count(&self) -> usize { + self.token_count.into() + } + pub fn serum3_count(&self) -> usize { + self.serum3_count.into() + } + pub fn perp_count(&self) -> usize { + self.perp_count.into() + } + pub fn perp_oo_count(&self) -> usize { + self.perp_oo_count.into() + } +} + +pub type MangoAccountValue = DynamicAccountValue; +pub type MangoAccountRef<'a> = DynamicAccountRef<'a, MangoAccount>; +pub type MangoAccountRefMut<'a> = DynamicAccountRefMut<'a, MangoAccount>; +pub type MangoAccountRefWithHeader<'a> = + DynamicAccount; + +impl MangoAccountValue { + // bytes without discriminator + pub fn from_bytes(bytes: &[u8]) -> Result { + let (fixed, dynamic) = bytes.split_at(size_of::()); + Ok(Self { + fixed: *bytemuck::from_bytes(&fixed), + header: MangoAccountDynamicHeader::from_bytes(dynamic)?, + dynamic: dynamic.to_vec(), + }) + } +} + +impl<'a> MangoAccountRefWithHeader<'a> { + // bytes without discriminator + pub fn from_bytes(bytes: &'a [u8]) -> Result { + let (fixed, dynamic) = bytes.split_at(size_of::()); + Ok(Self { + fixed: bytemuck::from_bytes(&fixed), + header: MangoAccountDynamicHeader::from_bytes(dynamic)?, + dynamic, + }) + } +} + +// This generic impl covers MangoAccountRef, MangoAccountRefMut and other +// DynamicAccountValue variants that allow read access. +impl< + Header: DerefOrBorrow, + Fixed: DerefOrBorrow, + Dynamic: DerefOrBorrow<[u8]>, + > DynamicAccount +{ + fn header(&self) -> &MangoAccountDynamicHeader { + self.header.deref_or_borrow() + } + fn fixed(&self) -> &MangoAccountFixed { + self.fixed.deref_or_borrow() + } + fn dynamic(&self) -> &[u8] { + self.dynamic.deref_or_borrow() + } + + /// Returns + /// - the position + /// - the raw index into the token positions list (for use with get_raw/deactivate) + pub fn token_get(&self, token_index: TokenIndex) -> Result<(&TokenPosition, usize)> { + self.token_iter() + .enumerate() + .find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index))) + .ok_or_else(|| error_msg!("position for token index {} not found", token_index)) + } + + // get TokenPosition at raw_index + pub fn token_get_raw(&self, raw_index: usize) -> &TokenPosition { + get_helper(self.dynamic(), self.header().token_offset(raw_index)) + } + + // get iter over all TokenPositions (including inactive) + pub fn token_iter(&self) -> impl Iterator + '_ { + (0..self.header().token_count()).map(|i| self.token_get_raw(i)) + } + + // get iter over all active TokenPositions + pub fn token_iter_active(&self) -> impl Iterator + '_ { + (0..self.header().token_count()) + .map(|i| self.token_get_raw(i)) + .filter(|token| token.is_active()) + } + + pub fn token_find(&self, token_index: TokenIndex) -> Option<&TokenPosition> { + self.token_iter_active() + .find(|p| p.is_active_for_token(token_index)) + } + + // get Serum3Orders at raw_index + pub fn serum3_get_raw(&self, raw_index: usize) -> &Serum3Orders { + get_helper(self.dynamic(), self.header().serum3_offset(raw_index)) + } + + pub fn serum3_iter(&self) -> impl Iterator + '_ { + (0..self.header().serum3_count()).map(|i| self.serum3_get_raw(i)) + } + + pub fn serum3_iter_active(&self) -> impl Iterator + '_ { + (0..self.header().serum3_count()) + .map(|i| self.serum3_get_raw(i)) + .filter(|serum3_order| serum3_order.is_active()) + } + + pub fn serum3_find(&self, market_index: Serum3MarketIndex) -> Option<&Serum3Orders> { + self.serum3_iter_active() + .find(|p| p.is_active_for_market(market_index)) + } + + // get PerpPosition at raw_index + pub fn perp_get_raw(&self, raw_index: usize) -> &PerpPositions { + get_helper(self.dynamic(), self.header().perp_offset(raw_index)) + } + + pub fn perp_iter(&self) -> impl Iterator { + (0..self.header().perp_count()).map(|i| self.perp_get_raw(i)) + } + + pub fn perp_iter_active_accounts(&self) -> impl Iterator { + (0..self.header().perp_count()) + .map(|i| self.perp_get_raw(i)) + .filter(|p| p.is_active()) + } + + pub fn perp_find_account(&self, market_index: PerpMarketIndex) -> Option<&PerpPositions> { + self.perp_iter_active_accounts() + .find(|p| p.is_active_for_market(market_index)) + } + + pub fn perp_oo_get_raw(&self, raw_index: usize) -> &PerpOpenOrders { + get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index)) + } + + pub fn perp_oo_iter(&self) -> impl Iterator { + (0..self.header().perp_oo_count()).map(|i| self.perp_oo_get_raw(i)) + } + + pub fn perp_next_order_slot(&self) -> Option { + self.perp_oo_iter() + .position(|&oo| oo.order_market == FREE_ORDER_SLOT) + } + + pub fn perp_find_order_with_client_order_id( + &self, + market_index: PerpMarketIndex, + client_order_id: u64, + ) -> Option<(i128, Side)> { + for i in 0..self.header().perp_oo_count() { + let oo = self.perp_oo_get_raw(i); + if oo.order_market == market_index && oo.client_order_id == client_order_id { + return Some((oo.order_id, oo.order_side)); + } + } + None + } + + pub fn perp_find_order_side( + &self, + market_index: PerpMarketIndex, + order_id: i128, + ) -> Option { + for i in 0..self.header().perp_oo_count() { + let oo = self.perp_oo_get_raw(i); + if oo.order_market == market_index && oo.order_id == order_id { + return Some(oo.order_side); + } + } + None + } + + pub fn being_liquidated(&self) -> bool { + self.fixed().being_liquidated() + } + + pub fn is_bankrupt(&self) -> bool { + self.fixed().is_bankrupt() + } + + pub fn borrow<'b>(&'b self) -> DynamicAccountRef<'b, MangoAccount> { + DynamicAccount { + header: self.header(), + fixed: self.fixed(), + dynamic: self.dynamic(), + } + } + + pub fn size(&self) -> AccountSize { + if self.header().perp_count() > 4 { + return AccountSize::Large; + } + AccountSize::Small + } +} + +impl< + Header: DerefOrBorrowMut + DerefOrBorrow, + Fixed: DerefOrBorrowMut + DerefOrBorrow, + Dynamic: DerefOrBorrowMut<[u8]> + DerefOrBorrow<[u8]>, + > DynamicAccount +{ + fn header_mut(&mut self) -> &mut MangoAccountDynamicHeader { + self.header.deref_or_borrow_mut() + } + fn dynamic_mut(&mut self) -> &mut [u8] { + self.dynamic.deref_or_borrow_mut() + } + + pub fn borrow_mut<'b>(&'b mut self) -> DynamicAccountRefMut<'b, MangoAccount> { + DynamicAccount { + header: self.header.deref_or_borrow_mut(), + fixed: self.fixed.deref_or_borrow_mut(), + dynamic: self.dynamic.deref_or_borrow_mut(), + } + } + + /// Returns + /// - the position + /// - the raw index into the token positions list (for use with get_raw/deactivate) + pub fn token_get_mut( + &mut self, + token_index: TokenIndex, + ) -> Result<(&mut TokenPosition, usize)> { + let raw_index = self + .token_iter() + .enumerate() + .find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| raw_index)) + .ok_or_else(|| error_msg!("position for token index {} not found", token_index))?; + Ok((self.token_get_mut_raw(raw_index), raw_index)) + } + + // get mut TokenPosition at raw_index + pub fn token_get_mut_raw(&mut self, raw_index: usize) -> &mut TokenPosition { + let offset = self.header().token_offset(raw_index); + get_helper_mut(self.dynamic_mut(), offset) + } + + /// Creates or retrieves a TokenPosition for the token_index. + /// Returns: + /// - the position + /// - the raw index into the token positions list (for use with get_raw) + /// - the active index, for use with FixedOrderAccountRetriever + pub fn token_get_mut_or_create( + &mut self, + token_index: TokenIndex, + ) -> Result<(&mut TokenPosition, usize, usize)> { + let mut active_index = 0; + let mut match_or_free = None; + for (raw_index, position) in self.token_iter().enumerate() { + if position.is_active_for_token(token_index) { + // Can't return early because of lifetimes + match_or_free = Some((raw_index, active_index)); + break; + } + if position.is_active() { + active_index += 1; + } else if match_or_free.is_none() { + match_or_free = Some((raw_index, active_index)); + } + } + if let Some((raw_index, bank_index)) = match_or_free { + let v = self.token_get_mut_raw(raw_index); + if !v.is_active_for_token(token_index) { + *v = TokenPosition { + indexed_position: I80F48::ZERO, + token_index, + in_use_count: 0, + reserved: Default::default(), + }; + } + Ok((v, raw_index, bank_index)) + } else { + err!(MangoError::NoFreeTokenPositionIndex) + .context(format!("when looking for token index {}", token_index)) + } + } + + pub fn token_deactivate(&mut self, raw_index: usize) { + assert!(self.token_get_mut_raw(raw_index).in_use_count == 0); + self.token_get_mut_raw(raw_index).token_index = TokenIndex::MAX; + } + + // get mut Serum3Orders at raw_index + pub fn serum3_get_mut_raw(&mut self, raw_index: usize) -> &mut Serum3Orders { + let offset = self.header().serum3_offset(raw_index); + get_helper_mut(self.dynamic_mut(), offset) + } + + pub fn serum3_create(&mut self, market_index: Serum3MarketIndex) -> Result<&mut Serum3Orders> { + if self.serum3_find(market_index).is_some() { + return err!(MangoError::Serum3OpenOrdersExistAlready); + } + + let raw_index_opt = self.serum3_iter().position(|p| !p.is_active()); + if let Some(raw_index) = raw_index_opt { + *(self.serum3_get_mut_raw(raw_index)) = Serum3Orders { + market_index: market_index as Serum3MarketIndex, + ..Serum3Orders::default() + }; + return Ok(self.serum3_get_mut_raw(raw_index)); + } else { + return err!(MangoError::NoFreeSerum3OpenOrdersIndex); + } + } + + pub fn serum3_deactivate(&mut self, market_index: Serum3MarketIndex) -> Result<()> { + let raw_index = self + .serum3_iter() + .position(|p| p.is_active_for_market(market_index)) + .ok_or_else(|| error_msg!("serum3 open orders index {} not found", market_index))?; + self.serum3_get_mut_raw(raw_index).market_index = Serum3MarketIndex::MAX; + Ok(()) + } + + pub fn serum3_find_mut( + &mut self, + market_index: Serum3MarketIndex, + ) -> Option<&mut Serum3Orders> { + let raw_index_opt = self + .serum3_iter_active() + .position(|p| p.is_active_for_market(market_index)); + raw_index_opt.map(|raw_index| self.serum3_get_mut_raw(raw_index)) + } + + // get mut PerpPosition at raw_index + pub fn perp_get_mut_raw(&mut self, raw_index: usize) -> &mut PerpPositions { + let offset = self.header().perp_offset(raw_index); + get_helper_mut(self.dynamic_mut(), offset) + } + + pub fn perp_oo_get_mut_raw(&mut self, raw_index: usize) -> &mut PerpOpenOrders { + let offset = self.header().perp_oo_offset(raw_index); + get_helper_mut(self.dynamic_mut(), offset) + } + + pub fn perp_get_account_mut_or_create( + &mut self, + perp_market_index: PerpMarketIndex, + ) -> Result<(&mut PerpPositions, usize)> { + let mut raw_index_opt = self + .perp_iter_active_accounts() + .position(|p| p.is_active_for_market(perp_market_index)); + if raw_index_opt.is_none() { + raw_index_opt = self.perp_iter().position(|p| !p.is_active()); + if let Some(raw_index) = raw_index_opt { + *(self.perp_get_mut_raw(raw_index)) = PerpPositions { + market_index: perp_market_index, + ..Default::default() + }; + } + } + if let Some(raw_index) = raw_index_opt { + Ok((self.perp_get_mut_raw(raw_index), raw_index)) + } else { + err!(MangoError::NoFreePerpPositionIndex) + } + } + + pub fn perp_deactivate_account(&mut self, raw_index: usize) { + self.perp_get_mut_raw(raw_index).market_index = PerpMarketIndex::MAX; + } + + pub fn perp_add_order( + &mut self, + perp_market_index: PerpMarketIndex, + side: Side, + order: &LeafNode, + ) -> Result<()> { + let mut perp_account = self + .perp_get_account_mut_or_create(perp_market_index) + .unwrap() + .0; + match side { + Side::Bid => { + perp_account.bids_base_lots = cm!(perp_account.bids_base_lots + order.quantity); + } + Side::Ask => { + perp_account.asks_base_lots = cm!(perp_account.asks_base_lots + order.quantity); + } + }; + let slot = order.owner_slot as usize; + + let mut oo = self.perp_oo_get_mut_raw(slot); + oo.order_market = perp_market_index; + oo.order_side = side; + oo.order_id = order.key; + oo.client_order_id = order.client_order_id; + Ok(()) + } + + pub fn perp_remove_order(&mut self, slot: usize, quantity: i64) -> Result<()> { + { + let oo = self.perp_oo_get_mut_raw(slot); + require_neq!(oo.order_market, FREE_ORDER_SLOT); + let order_side = oo.order_side; + let perp_market_index = oo.order_market; + let perp_account = self + .perp_get_account_mut_or_create(perp_market_index) + .unwrap() + .0; + + // accounting + match order_side { + Side::Bid => { + perp_account.bids_base_lots = cm!(perp_account.bids_base_lots - quantity); + } + Side::Ask => { + perp_account.asks_base_lots = cm!(perp_account.asks_base_lots - quantity); + } + } + } + + // release space + let oo = self.perp_oo_get_mut_raw(slot); + oo.order_market = FREE_ORDER_SLOT; + oo.order_side = Side::Bid; + oo.order_id = 0i128; + oo.client_order_id = 0u64; + Ok(()) + } + + pub fn perp_execute_maker( + &mut self, + perp_market_index: PerpMarketIndex, + perp_market: &mut PerpMarket, + fill: &FillEvent, + ) -> Result<()> { + let pa = self + .perp_get_account_mut_or_create(perp_market_index) + .unwrap() + .0; + pa.settle_funding(perp_market); + + let side = fill.taker_side.invert_side(); + let (base_change, quote_change) = fill.base_quote_change(side); + pa.change_base_position(perp_market, base_change); + let quote = I80F48::from_num( + perp_market + .quote_lot_size + .checked_mul(quote_change) + .unwrap(), + ); + let fees = quote.abs() * fill.maker_fee; + if !fill.market_fees_applied { + perp_market.fees_accrued += fees; + } + pa.quote_position_native = pa.quote_position_native.checked_add(quote - fees).unwrap(); + + if fill.maker_out { + self.perp_remove_order(fill.maker_slot as usize, base_change.abs()) + } else { + match side { + Side::Bid => { + pa.bids_base_lots = cm!(pa.bids_base_lots - base_change.abs()); + } + Side::Ask => { + pa.asks_base_lots = cm!(pa.asks_base_lots - base_change.abs()); + } + } + Ok(()) + } + } + + pub fn perp_execute_taker( + &mut self, + perp_market_index: PerpMarketIndex, + perp_market: &mut PerpMarket, + fill: &FillEvent, + ) -> Result<()> { + let pa = self + .perp_get_account_mut_or_create(perp_market_index) + .unwrap() + .0; + pa.settle_funding(perp_market); + + let (base_change, quote_change) = fill.base_quote_change(fill.taker_side); + pa.remove_taker_trade(base_change, quote_change); + pa.change_base_position(perp_market, base_change); + let quote = I80F48::from_num(perp_market.quote_lot_size * quote_change); + + // fees are assessed at time of trade; no need to assess fees here + + pa.quote_position_native += quote; + Ok(()) + } + + // writes length of tokens vec at appropriate offset so that borsh can infer the vector length + // length used is that present in the header + fn write_token_length(&mut self) { + let tokens_offset = self.header().token_offset(0); + // msg!( + // "writing tokens length at {}", + // tokens_offset - size_of::() + // ); + let count = self.header().token_count; + let dst: &mut [u8] = + &mut self.dynamic_mut()[tokens_offset - BORSH_VEC_SIZE_BYTES..tokens_offset]; + dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); + } + + fn write_serum3_length(&mut self) { + let serum3_offset = self.header().serum3_offset(0); + // msg!( + // "writing serum3 length at {}", + // serum3_offset - size_of::() + // ); + let count = self.header().serum3_count; + let dst: &mut [u8] = + &mut self.dynamic_mut()[serum3_offset - BORSH_VEC_SIZE_BYTES..serum3_offset]; + dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); + } + + fn write_perp_length(&mut self) { + let perp_offset = self.header().perp_offset(0); + // msg!( + // "writing perp length at {}", + // perp_offset - size_of::() + // ); + let count = self.header().perp_count; + let dst: &mut [u8] = + &mut self.dynamic_mut()[perp_offset - BORSH_VEC_SIZE_BYTES..perp_offset]; + dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); + } + + fn write_perp_oo_length(&mut self) { + let perp_oo_offset = self.header().perp_oo_offset(0); + // msg!( + // "writing perp length at {}", + // perp_offset - size_of::() + // ); + let count = self.header().perp_oo_count; + let dst: &mut [u8] = + &mut self.dynamic_mut()[perp_oo_offset - BORSH_VEC_SIZE_BYTES..perp_oo_offset]; + dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes()); + } + + pub fn expand_dynamic_content(&mut self, account_size: AccountSize) -> Result<()> { + let (new_token_count, new_serum3_count, new_perp_count, new_perp_oo_count) = + account_size.space(); + + require_gt!(new_token_count, self.header().token_count); + require_gt!(new_serum3_count, self.header().serum3_count); + require_gt!(new_perp_count, self.header().perp_count); + require_gt!(new_perp_oo_count, self.header().perp_oo_count); + + // create a temp copy to compute new starting offsets + let new_header = MangoAccountDynamicHeader { + token_count: new_token_count, + serum3_count: new_serum3_count, + perp_count: new_perp_count, + perp_oo_count: new_perp_oo_count, + }; + let old_header = self.header().clone(); + let dynamic = self.dynamic_mut(); + + // expand dynamic components by first moving existing positions, and then setting new ones to defaults + + // perp oo + unsafe { + sol_memmove( + &mut dynamic[new_header.perp_oo_offset(0)], + &mut dynamic[old_header.perp_oo_offset(0)], + size_of::() * old_header.perp_oo_count(), + ); + } + for i in old_header.perp_oo_count..new_perp_oo_count { + *get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) = + PerpOpenOrders::default(); + } + + // perp positions + unsafe { + sol_memmove( + &mut dynamic[new_header.perp_offset(0)], + &mut dynamic[old_header.perp_offset(0)], + size_of::() * old_header.perp_count(), + ); + } + for i in old_header.perp_count..new_perp_count { + *get_helper_mut(dynamic, new_header.perp_offset(i.into())) = PerpPositions::default(); + } + + // serum3 positions + unsafe { + sol_memmove( + &mut dynamic[new_header.serum3_offset(0)], + &mut dynamic[old_header.serum3_offset(0)], + size_of::() * old_header.serum3_count(), + ); + } + for i in old_header.serum3_count..new_serum3_count { + *get_helper_mut(dynamic, new_header.serum3_offset(i.into())) = Serum3Orders::default(); + } + + // token positions + unsafe { + sol_memmove( + &mut dynamic[new_header.token_offset(0)], + &mut dynamic[old_header.token_offset(0)], + size_of::() * old_header.token_count(), + ); + } + for i in old_header.token_count..new_token_count { + *get_helper_mut(dynamic, new_header.token_offset(i.into())) = TokenPosition::default(); + } + + // update header + let header_mut = self.header_mut(); + header_mut.token_count = new_token_count; + header_mut.serum3_count = new_serum3_count; + header_mut.perp_count = new_perp_count; + header_mut.perp_oo_count = new_perp_oo_count; + + // write new lengths (uses header) + self.write_token_length(); + self.write_serum3_length(); + self.write_perp_length(); + self.write_perp_oo_length(); + + Ok(()) + } +} diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs new file mode 100644 index 000000000..aa06ee7e7 --- /dev/null +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -0,0 +1,296 @@ +use anchor_lang::prelude::*; +use checked_math as cm; +use fixed::types::I80F48; +use static_assertions::const_assert_eq; +use std::cmp::Ordering; +use std::mem::size_of; + +use crate::state::*; + +pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX; + +#[zero_copy] +#[derive(AnchorDeserialize, AnchorSerialize, Debug)] +pub struct TokenPosition { + // TODO: Why did we have deposits and borrows as two different values + // if only one of them was allowed to be != 0 at a time? + // todo: maybe we want to split collateral and lending? + // todo: see https://github.com/blockworks-foundation/mango-v4/issues/1 + // todo: how does ftx do this? + /// The deposit_index (if positive) or borrow_index (if negative) scaled position + pub indexed_position: I80F48, + + /// index into Group.tokens + pub token_index: TokenIndex, + + /// incremented when a market requires this position to stay alive + pub in_use_count: u8, + + pub reserved: [u8; 5], +} + +unsafe impl bytemuck::Pod for TokenPosition {} +unsafe impl bytemuck::Zeroable for TokenPosition {} + +const_assert_eq!(size_of::(), 24); +const_assert_eq!(size_of::() % 8, 0); + +impl Default for TokenPosition { + fn default() -> Self { + TokenPosition { + indexed_position: I80F48::ZERO, + token_index: TokenIndex::MAX, + in_use_count: 0, + reserved: Default::default(), + } + } +} + +impl TokenPosition { + pub fn is_active(&self) -> bool { + self.token_index != TokenIndex::MAX + } + + pub fn is_active_for_token(&self, token_index: TokenIndex) -> bool { + self.token_index == token_index + } + + pub fn native(&self, bank: &Bank) -> I80F48 { + if self.indexed_position.is_positive() { + self.indexed_position * bank.deposit_index + } else { + self.indexed_position * bank.borrow_index + } + } + + pub fn ui(&self, bank: &Bank) -> I80F48 { + if self.indexed_position.is_positive() { + (self.indexed_position * bank.deposit_index) + / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) + } else { + (self.indexed_position * bank.borrow_index) + / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) + } + } + + pub fn is_in_use(&self) -> bool { + self.in_use_count > 0 + } +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +pub struct Serum3Orders { + pub open_orders: Pubkey, + + // tracks reserved funds in open orders account, + // used for bookkeeping of potentital loans which + // can be charged with loan origination fees + // e.g. serum3 settle funds ix + pub previous_native_coin_reserved: u64, + pub previous_native_pc_reserved: u64, + + pub market_index: Serum3MarketIndex, + + /// Store the base/quote token index, so health computations don't need + /// to get passed the static SerumMarket to find which tokens a market + /// uses and look up the correct oracles. + pub base_token_index: TokenIndex, + pub quote_token_index: TokenIndex, + + pub reserved: [u8; 2], +} +const_assert_eq!(size_of::(), 32 + 8 * 2 + 2 * 3 + 2); // 56 +const_assert_eq!(size_of::() % 8, 0); + +unsafe impl bytemuck::Pod for Serum3Orders {} +unsafe impl bytemuck::Zeroable for Serum3Orders {} + +impl Serum3Orders { + pub fn is_active(&self) -> bool { + self.market_index != Serum3MarketIndex::MAX + } + + pub fn is_active_for_market(&self, market_index: Serum3MarketIndex) -> bool { + self.market_index == market_index + } +} + +impl Default for Serum3Orders { + fn default() -> Self { + Self { + open_orders: Pubkey::default(), + market_index: Serum3MarketIndex::MAX, + base_token_index: TokenIndex::MAX, + quote_token_index: TokenIndex::MAX, + reserved: Default::default(), + previous_native_coin_reserved: 0, + previous_native_pc_reserved: 0, + } + } +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct PerpPositions { + pub market_index: PerpMarketIndex, + pub reserved: [u8; 6], + + /// Active position size, measured in base lots + pub base_position_lots: i64, + /// Active position in quote (conversation rate is that of the time the order was settled) + /// measured in native quote + pub quote_position_native: I80F48, + + /// Already settled funding + pub long_settled_funding: I80F48, + pub short_settled_funding: I80F48, + + /// Base lots in bids + pub bids_base_lots: i64, + /// Base lots in asks + pub asks_base_lots: i64, + + /// Liquidity mining rewards + // pub mngo_accrued: u64, + + /// Amount that's on EventQueue waiting to be processed + pub taker_base_lots: i64, + pub taker_quote_lots: i64, +} + +impl std::fmt::Debug for PerpPositions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PerpAccount") + .field("market_index", &self.market_index) + .field("base_position_lots", &self.base_position_lots) + .field("quote_position_native", &self.quote_position_native) + .field("bids_base_lots", &self.bids_base_lots) + .field("asks_base_lots", &self.asks_base_lots) + .field("taker_base_lots", &self.taker_base_lots) + .field("taker_quote_lots", &self.taker_quote_lots) + .finish() + } +} +const_assert_eq!(size_of::(), 8 + 8 * 5 + 3 * 16); // 96 +const_assert_eq!(size_of::() % 8, 0); + +unsafe impl bytemuck::Pod for PerpPositions {} +unsafe impl bytemuck::Zeroable for PerpPositions {} + +impl Default for PerpPositions { + fn default() -> Self { + Self { + market_index: PerpMarketIndex::MAX, + base_position_lots: 0, + quote_position_native: I80F48::ZERO, + bids_base_lots: 0, + asks_base_lots: 0, + taker_base_lots: 0, + taker_quote_lots: 0, + reserved: Default::default(), + long_settled_funding: I80F48::ZERO, + short_settled_funding: I80F48::ZERO, + } + } +} + +impl PerpPositions { + /// Add taker trade after it has been matched but before it has been process on EventQueue + pub fn add_taker_trade(&mut self, side: Side, base_lots: i64, quote_lots: i64) { + match side { + Side::Bid => { + self.taker_base_lots = cm!(self.taker_base_lots + base_lots); + self.taker_quote_lots = cm!(self.taker_quote_lots - quote_lots); + } + Side::Ask => { + self.taker_base_lots = cm!(self.taker_base_lots - base_lots); + self.taker_quote_lots = cm!(self.taker_quote_lots + quote_lots); + } + } + } + /// Remove taker trade after it has been processed on EventQueue + pub fn remove_taker_trade(&mut self, base_change: i64, quote_change: i64) { + self.taker_base_lots = cm!(self.taker_base_lots - base_change); + self.taker_quote_lots = cm!(self.taker_quote_lots - quote_change); + } + + pub fn is_active(&self) -> bool { + self.market_index != PerpMarketIndex::MAX + } + + pub fn is_active_for_market(&self, market_index: PerpMarketIndex) -> bool { + self.market_index == market_index + } + + /// This assumes settle_funding was already called + pub fn change_base_position(&mut self, perp_market: &mut PerpMarket, base_change: i64) { + let start = self.base_position_lots; + self.base_position_lots += base_change; + perp_market.open_interest += self.base_position_lots.abs() - start.abs(); + } + + /// Move unrealized funding payments into the quote_position + pub fn settle_funding(&mut self, perp_market: &PerpMarket) { + match self.base_position_lots.cmp(&0) { + Ordering::Greater => { + self.quote_position_native -= (perp_market.long_funding + - self.long_settled_funding) + * I80F48::from_num(self.base_position_lots); + } + Ordering::Less => { + self.quote_position_native -= (perp_market.short_funding + - self.short_settled_funding) + * I80F48::from_num(self.base_position_lots); + } + Ordering::Equal => (), + } + self.long_settled_funding = perp_market.long_funding; + self.short_settled_funding = perp_market.short_funding; + } +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +pub struct PerpOpenOrders { + pub order_side: Side, // TODO: storing enums isn't POD + pub reserved1: [u8; 1], + pub order_market: PerpMarketIndex, + pub reserved2: [u8; 4], + pub client_order_id: u64, + pub order_id: i128, +} + +impl Default for PerpOpenOrders { + fn default() -> Self { + Self { + order_side: Side::Bid, + reserved1: Default::default(), + order_market: FREE_ORDER_SLOT, + reserved2: Default::default(), + client_order_id: 0, + order_id: 0, + } + } +} + +unsafe impl bytemuck::Pod for PerpOpenOrders {} +unsafe impl bytemuck::Zeroable for PerpOpenOrders {} + +const_assert_eq!(size_of::(), 1 + 1 + 2 + 4 + 8 + 16); +const_assert_eq!(size_of::() % 8, 0); + +#[macro_export] +macro_rules! account_seeds { + ( $account:expr ) => { + &[ + $account.group.as_ref(), + b"MangoAccount".as_ref(), + $account.owner.as_ref(), + &$account.account_num.to_le_bytes(), + &[$account.bump], + ] + }; +} + +pub use account_seeds; diff --git a/programs/mango-v4/src/state/mod.rs b/programs/mango-v4/src/state/mod.rs index 2ac840853..9b13bc3cd 100644 --- a/programs/mango-v4/src/state/mod.rs +++ b/programs/mango-v4/src/state/mod.rs @@ -1,8 +1,10 @@ pub use bank::*; +pub use dynamic_account::*; pub use equity::*; pub use group::*; pub use health::*; pub use mango_account::*; +pub use mango_account_components::*; pub use mint_info::*; pub use oracle::*; pub use orderbook::*; @@ -10,10 +12,12 @@ pub use perp_market::*; pub use serum3_market::*; mod bank; +mod dynamic_account; mod equity; mod group; mod health; mod mango_account; +mod mango_account_components; mod mint_info; mod oracle; mod orderbook; diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index aee990659..94559f9a2 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -10,7 +10,7 @@ use switchboard_v2::AggregatorAccountData; use crate::accounts_zerocopy::*; use crate::checked_math as cm; -use crate::error::MangoError; +use crate::error::*; const LOOKUP_START: i8 = -12; const LOOKUP: [I80F48; 25] = [ @@ -144,14 +144,21 @@ pub fn oracle_price( cm!(price * decimal_adj) } OracleType::SwitchboardV2 => { + fn from_foreign_error(e: impl std::fmt::Display) -> Error { + error_msg!("{}", e) + } + let feed = bytemuck::from_bytes::(&data[8..]); - let feed_result = feed.get_result()?; - let price_decimal: f64 = feed_result.try_into()?; + let feed_result = feed.get_result().map_err(from_foreign_error)?; + let price_decimal: f64 = feed_result.try_into().map_err(from_foreign_error)?; let price = I80F48::from_num(price_decimal); // Filter out bad prices - let std_deviation_decimal: f64 = - feed.latest_confirmed_round.std_deviation.try_into()?; + let std_deviation_decimal: f64 = feed + .latest_confirmed_round + .std_deviation + .try_into() + .map_err(from_foreign_error)?; if I80F48::from_num(std_deviation_decimal) > oracle_conf_filter * price { msg!( "Switchboard v2 std deviation too high; pubkey {} price: {} latest_confirmed_round.std_deviation: {}", diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index 014ddea64..50ae44d6e 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -1,12 +1,12 @@ use std::cell::RefMut; use crate::accounts_zerocopy::*; +use crate::state::MangoAccountRefMut; use crate::{ error::MangoError, state::{ orderbook::{bookside::BookSide, nodes::LeafNode}, - EventQueue, MangoAccount, MangoAccountPerpPositions, PerpMarket, FREE_ORDER_SLOT, - MAX_PERP_OPEN_ORDERS, + EventQueue, PerpMarket, FREE_ORDER_SLOT, }, }; use anchor_lang::prelude::*; @@ -159,7 +159,7 @@ impl<'a> Book<'a> { perp_market: &mut PerpMarket, event_queue: &mut EventQueue, oracle_price: I80F48, - mango_account_perps: &mut MangoAccountPerpPositions, + mango_account: &mut MangoAccountRefMut, mango_account_pk: &Pubkey, price_lots: i64, max_base_lots: i64, @@ -264,8 +264,8 @@ impl<'a> Book<'a> { // Record the taker trade in the account already, even though it will only be // realized when the fill event gets executed - let perp_account = mango_account_perps - .get_account_mut_or_create(market.perp_market_index)? + let perp_account = mango_account + .perp_get_account_mut_or_create(market.perp_market_index)? .0; perp_account.add_taker_trade(side, match_base_lots, match_quote_lots); @@ -346,8 +346,8 @@ impl<'a> Book<'a> { event_queue.push_back(cast(event)).unwrap(); } - let owner_slot = mango_account_perps - .next_order_slot() + let owner_slot = mango_account + .perp_next_order_slot() .ok_or_else(|| error!(MangoError::SomeError))?; let new_order = LeafNode::new( owner_slot as u8, @@ -373,13 +373,13 @@ impl<'a> Book<'a> { price_lots ); - mango_account_perps.add_order(market.perp_market_index, side, &new_order)?; + mango_account.perp_add_order(market.perp_market_index, side, &new_order)?; } // if there were matched taker quote apply ref fees // we know ref_fee_rate is not None if total_quote_taken > 0 if total_quote_lots_taken > 0 { - apply_fees(market, mango_account_perps, total_quote_lots_taken)?; + apply_fees(market, mango_account, total_quote_lots_taken)?; } Ok(()) @@ -387,20 +387,21 @@ impl<'a> Book<'a> { pub fn cancel_all_order( &mut self, - mango_account: &mut MangoAccount, + mango_account: &mut MangoAccountRefMut, perp_market: &mut PerpMarket, mut limit: u8, side_to_cancel_option: Option, ) -> Result<()> { - for i in 0..MAX_PERP_OPEN_ORDERS { - if mango_account.perps.order_market[i] == FREE_ORDER_SLOT - || mango_account.perps.order_market[i] != perp_market.perp_market_index + for i in 0..mango_account.header.perp_oo_count() { + let oo = mango_account.perp_oo_get_raw(i); + if oo.order_market == FREE_ORDER_SLOT + || oo.order_market != perp_market.perp_market_index { continue; } - let order_id = mango_account.perps.order_id[i]; - let order_side = mango_account.perps.order_side[i]; + let order_id = oo.order_id; + let order_side = oo.order_side; if let Some(side_to_cancel) = side_to_cancel_option { if side_to_cancel != order_side { @@ -410,8 +411,7 @@ impl<'a> Book<'a> { if let Ok(leaf_node) = self.cancel_order(order_id, order_side) { mango_account - .perps - .remove_order(leaf_node.owner_slot as usize, leaf_node.quantity)? + .perp_remove_order(leaf_node.owner_slot as usize, leaf_node.quantity)? }; limit -= 1; @@ -441,7 +441,7 @@ impl<'a> Book<'a> { /// both the maker and taker fees. fn apply_fees( market: &mut PerpMarket, - mango_account_perps: &mut MangoAccountPerpPositions, + mango_account: &mut MangoAccountRefMut, total_quote_taken: i64, ) -> Result<()> { let taker_quote_native = I80F48::from_num( @@ -458,8 +458,8 @@ fn apply_fees( let maker_fees = taker_quote_native * market.maker_fee; let taker_fees = taker_quote_native * market.taker_fee; - let perp_account = mango_account_perps - .get_account_mut_or_create(market.perp_market_index)? + let perp_account = mango_account + .perp_get_account_mut_or_create(market.perp_market_index)? .0; perp_account.quote_position_native -= taker_fees; market.fees_accrued += taker_fees + maker_fees; diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 93014bd0e..f34c45a4c 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -15,7 +15,8 @@ pub mod queue; #[cfg(test)] mod tests { use super::*; - use crate::state::{MangoAccountPerpPositions, PerpMarket, FREE_ORDER_SLOT}; + use crate::state::{MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT}; + use anchor_lang::prelude::*; use bytemuck::Zeroable; use fixed::types::I80F48; use solana_program::pubkey::Pubkey; @@ -99,7 +100,8 @@ mod tests { let mut new_order = |book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 { - let mut account_perps = MangoAccountPerpPositions::new(); + let buffer = MangoAccount::default().try_to_vec().unwrap(); + let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); let quantity = 1; let tif = 100; @@ -109,7 +111,7 @@ mod tests { &mut perp_market, event_queue, oracle_price, - &mut account_perps, + &mut account.borrow_mut(), &Pubkey::default(), price, quantity, @@ -121,7 +123,7 @@ mod tests { u8::MAX, ) .unwrap(); - account_perps.order_id[0] + account.perp_oo_get_raw(0).order_id }; // insert bids until book side is full @@ -193,8 +195,10 @@ mod tests { market.maker_fee = I80F48::from_num(-0.001f64); market.taker_fee = I80F48::from_num(0.01f64); - let mut maker = MangoAccountPerpPositions::new(); - let mut taker = MangoAccountPerpPositions::new(); + let buffer = MangoAccount::default().try_to_vec().unwrap(); + let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap(); + let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap(); + let maker_pk = Pubkey::new_unique(); let taker_pk = Pubkey::new_unique(); let now_ts = 1000000; @@ -207,7 +211,7 @@ mod tests { &mut market, &mut event_queue, oracle_price, - &mut maker, + &mut maker.borrow_mut(), &maker_pk, price, bid_quantity, @@ -219,19 +223,28 @@ mod tests { u8::MAX, ) .unwrap(); - assert_eq!(maker.order_market[0], market.perp_market_index); - assert_eq!(maker.order_market[1], FREE_ORDER_SLOT); - assert_ne!(maker.order_id[0], 0); - assert_eq!(maker.client_order_id[0], 42); - assert_eq!(maker.order_side[0], Side::Bid); - assert!(bookside_contains_key(&book.bids, maker.order_id[0])); + assert_eq!( + maker.perp_oo_get_mut_raw(0).order_market, + market.perp_market_index + ); + assert_eq!(maker.perp_oo_get_mut_raw(1).order_market, FREE_ORDER_SLOT); + assert_ne!(maker.perp_oo_get_mut_raw(0).order_id, 0); + assert_eq!(maker.perp_oo_get_mut_raw(0).client_order_id, 42); + assert_eq!(maker.perp_oo_get_mut_raw(0).order_side, Side::Bid); + assert!(bookside_contains_key( + &book.bids, + maker.perp_oo_get_mut_raw(0).order_id + )); assert!(bookside_contains_price(&book.bids, price)); - assert_eq!(maker.accounts[0].bids_base_lots, bid_quantity); - assert_eq!(maker.accounts[0].asks_base_lots, 0); - assert_eq!(maker.accounts[0].taker_base_lots, 0); - assert_eq!(maker.accounts[0].taker_quote_lots, 0); - assert_eq!(maker.accounts[0].base_position_lots, 0); - assert_eq!(maker.accounts[0].quote_position_native, 0); + assert_eq!(maker.perp_get_raw(0).bids_base_lots, bid_quantity); + assert_eq!(maker.perp_get_raw(0).asks_base_lots, 0); + assert_eq!(maker.perp_get_raw(0).taker_base_lots, 0); + assert_eq!(maker.perp_get_raw(0).taker_quote_lots, 0); + assert_eq!(maker.perp_get_raw(0).base_position_lots, 0); + assert_eq!( + maker.perp_get_raw(0).quote_position_native.to_num::(), + 0 + ); assert_eq!(event_queue.len(), 0); // Take the order partially @@ -241,7 +254,7 @@ mod tests { &mut market, &mut event_queue, oracle_price, - &mut taker, + &mut taker.borrow_mut(), &taker_pk, price, match_quantity, @@ -255,7 +268,7 @@ mod tests { .unwrap(); // the remainder of the maker order is still on the book // (the maker account is unchanged: it was not even passed in) - let order = bookside_leaf_by_key(&book.bids, maker.order_id[0]).unwrap(); + let order = bookside_leaf_by_key(&book.bids, maker.perp_oo_get_raw(0).order_id).unwrap(); assert_eq!(order.price(), price); assert_eq!(order.quantity, bid_quantity - match_quantity); @@ -267,14 +280,17 @@ mod tests { ); // the taker account is updated - assert_eq!(taker.order_market[0], FREE_ORDER_SLOT); - assert_eq!(taker.accounts[0].bids_base_lots, 0); - assert_eq!(taker.accounts[0].asks_base_lots, 0); - assert_eq!(taker.accounts[0].taker_base_lots, -match_quantity); - assert_eq!(taker.accounts[0].taker_quote_lots, match_quantity * price); - assert_eq!(taker.accounts[0].base_position_lots, 0); + assert_eq!(taker.perp_oo_get_raw(0).order_market, FREE_ORDER_SLOT); + assert_eq!(taker.perp_get_raw(0).bids_base_lots, 0); + assert_eq!(taker.perp_get_raw(0).asks_base_lots, 0); + assert_eq!(taker.perp_get_raw(0).taker_base_lots, -match_quantity); assert_eq!( - taker.accounts[0].quote_position_native, + taker.perp_get_raw(0).taker_quote_lots, + match_quantity * price + ); + assert_eq!(taker.perp_get_raw(0).base_position_lots, 0); + assert_eq!( + taker.perp_get_raw(0).quote_position_native, -match_quote * market.taker_fee ); @@ -294,34 +310,34 @@ mod tests { // simulate event queue processing maker - .execute_maker(market.perp_market_index, &mut market, &fill) + .perp_execute_maker(market.perp_market_index, &mut market, &fill) .unwrap(); taker - .execute_taker(market.perp_market_index, &mut market, &fill) + .perp_execute_taker(market.perp_market_index, &mut market, &fill) .unwrap(); assert_eq!(market.open_interest, 2 * match_quantity); - assert_eq!(maker.order_market[0], 0); + assert_eq!(maker.perp_oo_get_raw(0).order_market, 0); assert_eq!( - maker.accounts[0].bids_base_lots, + maker.perp_get_raw(0).bids_base_lots, bid_quantity - match_quantity ); - assert_eq!(maker.accounts[0].asks_base_lots, 0); - assert_eq!(maker.accounts[0].taker_base_lots, 0); - assert_eq!(maker.accounts[0].taker_quote_lots, 0); - assert_eq!(maker.accounts[0].base_position_lots, match_quantity); + assert_eq!(maker.perp_get_raw(0).asks_base_lots, 0); + assert_eq!(maker.perp_get_raw(0).taker_base_lots, 0); + assert_eq!(maker.perp_get_raw(0).taker_quote_lots, 0); + assert_eq!(maker.perp_get_raw(0).base_position_lots, match_quantity); assert_eq!( - maker.accounts[0].quote_position_native, + maker.perp_get_raw(0).quote_position_native, -match_quote - match_quote * market.maker_fee ); - assert_eq!(taker.accounts[0].bids_base_lots, 0); - assert_eq!(taker.accounts[0].asks_base_lots, 0); - assert_eq!(taker.accounts[0].taker_base_lots, 0); - assert_eq!(taker.accounts[0].taker_quote_lots, 0); - assert_eq!(taker.accounts[0].base_position_lots, -match_quantity); + assert_eq!(taker.perp_get_raw(0).bids_base_lots, 0); + assert_eq!(taker.perp_get_raw(0).asks_base_lots, 0); + assert_eq!(taker.perp_get_raw(0).taker_base_lots, 0); + assert_eq!(taker.perp_get_raw(0).taker_quote_lots, 0); + assert_eq!(taker.perp_get_raw(0).base_position_lots, -match_quantity); assert_eq!( - taker.accounts[0].quote_position_native, + taker.perp_get_raw(0).quote_position_native, match_quote - match_quote * market.taker_fee ); } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 089f2c429..2dd9f7455 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -9,6 +9,7 @@ use mango_v4::instructions::{ CpiData, FlashLoanWithdraw, InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side, }; +use mango_v4::state::{MangoAccount, MangoAccountValue}; use solana_program::instruction::Instruction; use solana_program_test::BanksClientError; use solana_sdk::instruction; @@ -28,6 +29,11 @@ pub trait ClientAccountLoader { let bytes = self.load_bytes(pubkey).await?; AccountDeserialize::try_deserialize(&mut &bytes[..]).ok() } + async fn load_mango_account(&self, pubkey: &Pubkey) -> Option { + self.load_bytes(pubkey) + .await + .map(|v| MangoAccountValue::from_bytes(&v[8..]).unwrap()) + } } #[async_trait::async_trait(?Send)] @@ -118,11 +124,15 @@ fn make_instruction( async fn get_mint_info_by_mint( account_loader: &impl ClientAccountLoader, - account: &MangoAccount, + account: &MangoAccountValue, mint: Pubkey, ) -> MintInfo { let mint_info_pk = Pubkey::find_program_address( - &[account.group.as_ref(), b"MintInfo".as_ref(), mint.as_ref()], + &[ + account.fixed.group.as_ref(), + b"MintInfo".as_ref(), + mint.as_ref(), + ], &mango_v4::id(), ) .0; @@ -131,12 +141,12 @@ async fn get_mint_info_by_mint( async fn get_mint_info_by_token_index( account_loader: &impl ClientAccountLoader, - account: &MangoAccount, + account: &MangoAccountValue, token_index: TokenIndex, ) -> MintInfo { let bank_pk = Pubkey::find_program_address( &[ - account.group.as_ref(), + account.fixed.group.as_ref(), b"Bank".as_ref(), &token_index.to_le_bytes(), &0u64.to_le_bytes(), @@ -163,7 +173,7 @@ fn get_perp_market_address_by_index(group: Pubkey, perp_market_index: PerpMarket // all the accounts that instructions like deposit/withdraw need to compute account health async fn derive_health_check_remaining_account_metas( account_loader: &impl ClientAccountLoader, - account: &MangoAccount, + account: &MangoAccountValue, affected_bank: Option, writable_banks: bool, affected_perp_market_index: Option, @@ -172,21 +182,19 @@ async fn derive_health_check_remaining_account_metas( if let Some(affected_bank) = affected_bank { let bank: Bank = account_loader.load(&affected_bank).await.unwrap(); adjusted_account - .tokens - .get_mut_or_create(bank.token_index) + .token_get_mut_or_create(bank.token_index) .unwrap(); } if let Some(affected_perp_market_index) = affected_perp_market_index { adjusted_account - .perps - .get_account_mut_or_create(affected_perp_market_index) + .perp_get_account_mut_or_create(affected_perp_market_index) .unwrap(); } // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; let mut oracles = vec![]; - for position in adjusted_account.tokens.iter_active() { + for position in adjusted_account.token_iter_active() { let mint_info = get_mint_info_by_token_index(account_loader, account, position.token_index).await; // TODO: ALTs are unavailable @@ -202,11 +210,10 @@ async fn derive_health_check_remaining_account_metas( } let perp_markets = adjusted_account - .perps - .iter_active_accounts() - .map(|perp| get_perp_market_address_by_index(account.group, perp.market_index)); + .perp_iter_active_accounts() + .map(|perp| get_perp_market_address_by_index(account.fixed.group, perp.market_index)); - let serum_oos = account.serum3.iter_active().map(|&s| s.open_orders); + let serum_oos = account.serum3_iter_active().map(|&s| s.open_orders); let to_account_meta = |pubkey| AccountMeta { pubkey, @@ -229,8 +236,8 @@ async fn derive_health_check_remaining_account_metas( async fn derive_liquidation_remaining_account_metas( account_loader: &impl ClientAccountLoader, - liqee: &MangoAccount, - liqor: &MangoAccount, + liqee: &MangoAccountValue, + liqor: &MangoAccountValue, asset_token_index: TokenIndex, asset_bank_index: usize, liab_token_index: TokenIndex, @@ -239,9 +246,8 @@ async fn derive_liquidation_remaining_account_metas( let mut banks = vec![]; let mut oracles = vec![]; let token_indexes = liqee - .tokens - .iter_active() - .chain(liqor.tokens.iter_active()) + .token_iter_active() + .chain(liqor.token_iter_active()) .map(|ta| ta.token_index) .unique(); for token_index in token_indexes { @@ -269,16 +275,14 @@ async fn derive_liquidation_remaining_account_metas( } let perp_markets = liqee - .perps - .iter_active_accounts() - .chain(liqee.perps.iter_active_accounts()) - .map(|perp| get_perp_market_address_by_index(liqee.group, perp.market_index)) + .perp_iter_active_accounts() + .chain(liqee.perp_iter_active_accounts()) + .map(|perp| get_perp_market_address_by_index(liqee.fixed.group, perp.market_index)) .unique(); let serum_oos = liqee - .serum3 - .iter_active() - .chain(liqor.serum3.iter_active()) + .serum3_iter_active() + .chain(liqor.serum3_iter_active()) .map(|&s| s.open_orders); let to_account_meta = |pubkey| AccountMeta { @@ -304,29 +308,32 @@ fn from_serum_style_pubkey(d: &[u64; 4]) -> Pubkey { Pubkey::new(bytemuck::cast_slice(d as &[_])) } +pub async fn get_mango_account(solana: &SolanaCookie, account: Pubkey) -> MangoAccountValue { + let bytes = solana.get_account_data(account).await.unwrap(); + MangoAccountValue::from_bytes(&bytes[8..]).unwrap() +} + pub async fn account_position(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> i64 { - let account_data: MangoAccount = solana.get_account(account).await; + let account_data = get_mango_account(solana, account).await; let bank_data: Bank = solana.get_account(bank).await; let native = account_data - .tokens - .find(bank_data.token_index) + .token_find(bank_data.token_index) .unwrap() .native(&bank_data); native.round().to_num::() } pub async fn account_position_closed(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> bool { - let account_data: MangoAccount = solana.get_account(account).await; + let account_data = get_mango_account(solana, account).await; let bank_data: Bank = solana.get_account(bank).await; - account_data.tokens.find(bank_data.token_index).is_none() + account_data.token_find(bank_data.token_index).is_none() } pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> f64 { - let account_data: MangoAccount = solana.get_account(account).await; + let account_data = get_mango_account(solana, account).await; let bank_data: Bank = solana.get_account(bank).await; let native = account_data - .tokens - .find(bank_data.token_index) + .token_find(bank_data.token_index) .unwrap() .native(&bank_data); native.to_num::() @@ -358,10 +365,13 @@ impl<'keypair> ClientInstruction for FlashLoanInstruction<'keypair> { ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, owner: self.owner.pubkey(), token_program: Token::id(), @@ -493,7 +503,10 @@ impl<'keypair> ClientInstruction for FlashLoan2EndInstruction<'keypair> { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let health_check_metas = derive_health_check_remaining_account_metas( &account_loader, @@ -505,7 +518,7 @@ impl<'keypair> ClientInstruction for FlashLoan2EndInstruction<'keypair> { .await; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, owner: self.owner.pubkey(), token_program: Token::id(), @@ -597,7 +610,10 @@ impl<'keypair> ClientInstruction for FlashLoan3EndInstruction<'keypair> { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let health_check_metas = derive_health_check_remaining_account_metas( &account_loader, @@ -660,10 +676,13 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> { // load accounts, find PDAs, find remainingAccounts let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap(); - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let mint_info = Pubkey::find_program_address( &[ - account.group.as_ref(), + account.fixed.group.as_ref(), b"MintInfo".as_ref(), token_account.mint.as_ref(), ], @@ -682,7 +701,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> { .await; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, owner: self.owner.pubkey(), bank: mint_info.banks[self.bank_index], @@ -725,10 +744,13 @@ impl ClientInstruction for TokenDepositInstruction { // load account so we know its mint let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap(); - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let mint_info = Pubkey::find_program_address( &[ - account.group.as_ref(), + account.fixed.group.as_ref(), b"MintInfo".as_ref(), token_account.mint.as_ref(), ], @@ -747,7 +769,7 @@ impl ClientInstruction for TokenDepositInstruction { .await; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, bank: mint_info.banks[self.bank_index], vault: mint_info.vaults[self.bank_index], @@ -800,7 +822,11 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> { ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); let instruction = Self::Instruction { - name: "some_ticker".to_string(), + name: format!( + "{}{}", + "some_ticker".to_string(), + self.token_index.to_string() + ), token_index: self.token_index, bank_num: 0, oracle_config: OracleConfig { @@ -1276,7 +1302,7 @@ impl<'keypair> ClientInstruction for GroupCloseInstruction<'keypair> { pub struct AccountCreateInstruction<'keypair> { pub account_num: u8, - + pub account_size: AccountSize, pub group: Pubkey, pub owner: &'keypair Keypair, pub payer: &'keypair Keypair, @@ -1292,6 +1318,7 @@ impl<'keypair> ClientInstruction for AccountCreateInstruction<'keypair> { let program_id = mango_v4::id(); let instruction = mango_v4::instruction::AccountCreate { account_num: self.account_num, + account_size: self.account_size, name: "my_mango_account".to_string(), }; @@ -1323,6 +1350,51 @@ impl<'keypair> ClientInstruction for AccountCreateInstruction<'keypair> { } } +pub struct AccountExpandInstruction<'keypair> { + pub account_num: u8, + pub group: Pubkey, + pub owner: &'keypair Keypair, + pub payer: &'keypair Keypair, +} +#[async_trait::async_trait(?Send)] +impl<'keypair> ClientInstruction for AccountExpandInstruction<'keypair> { + type Accounts = mango_v4::accounts::AccountExpand; + type Instruction = mango_v4::instruction::AccountExpand; + async fn to_instruction( + &self, + _account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = mango_v4::instruction::AccountExpand {}; + + let account = Pubkey::find_program_address( + &[ + self.group.as_ref(), + b"MangoAccount".as_ref(), + self.owner.pubkey().as_ref(), + &self.account_num.to_le_bytes(), + ], + &program_id, + ) + .0; + + let accounts = mango_v4::accounts::AccountExpand { + group: self.group, + account, + owner: self.owner.pubkey(), + payer: self.payer.pubkey(), + system_program: System::id(), + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec<&Keypair> { + vec![self.owner, self.payer] + } +} + pub struct AccountEditInstruction<'keypair> { pub account_num: u8, pub group: Pubkey, @@ -1641,11 +1713,13 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> { limit: self.limit, }; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap(); let open_orders = account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .unwrap() .open_orders; let quote_info = @@ -1686,7 +1760,7 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> { .await; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, open_orders, quote_bank: quote_info.first_bank(), @@ -1741,11 +1815,13 @@ impl<'keypair> ClientInstruction for Serum3CancelOrderInstruction<'keypair> { order_id: self.order_id, }; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap(); let open_orders = account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .unwrap() .open_orders; @@ -1762,7 +1838,7 @@ impl<'keypair> ClientInstruction for Serum3CancelOrderInstruction<'keypair> { let event_q = market_external.event_q; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, open_orders, serum_market: self.serum_market, @@ -1800,11 +1876,13 @@ impl<'keypair> ClientInstruction for Serum3CancelAllOrdersInstruction<'keypair> let program_id = mango_v4::id(); let instruction = Self::Instruction { limit: self.limit }; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap(); let open_orders = account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .unwrap() .open_orders; @@ -1821,7 +1899,7 @@ impl<'keypair> ClientInstruction for Serum3CancelAllOrdersInstruction<'keypair> let event_q = market_external.event_q; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, open_orders, serum_market: self.serum_market, @@ -1859,11 +1937,13 @@ impl<'keypair> ClientInstruction for Serum3SettleFundsInstruction<'keypair> { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap(); let open_orders = account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .unwrap() .open_orders; let quote_info = @@ -1891,7 +1971,7 @@ impl<'keypair> ClientInstruction for Serum3SettleFundsInstruction<'keypair> { .unwrap(); let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, open_orders, quote_bank: quote_info.first_bank(), @@ -1933,11 +2013,13 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction { let program_id = mango_v4::id(); let instruction = Self::Instruction { limit: self.limit }; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap(); let open_orders = account - .serum3 - .find(serum_market.market_index) + .serum3_find(serum_market.market_index) .unwrap() .open_orders; let quote_info = @@ -1977,7 +2059,7 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction { .await; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, open_orders, quote_bank: quote_info.first_bank(), @@ -2033,8 +2115,14 @@ impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> { max_liab_transfer: self.max_liab_transfer, }; - let liqee: MangoAccount = account_loader.load(&self.liqee).await.unwrap(); - let liqor: MangoAccount = account_loader.load(&self.liqor).await.unwrap(); + let liqee = account_loader + .load_mango_account(&self.liqee) + .await + .unwrap(); + let liqor = account_loader + .load_mango_account(&self.liqor) + .await + .unwrap(); let health_check_metas = derive_liquidation_remaining_account_metas( &account_loader, &liqee, @@ -2047,7 +2135,7 @@ impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> { .await; let accounts = Self::Accounts { - group: liqee.group, + group: liqee.fixed.group, liqee: self.liqee, liqor: self.liqor, liqor_owner: self.liqor_owner.pubkey(), @@ -2088,8 +2176,14 @@ impl<'keypair> ClientInstruction for LiqTokenBankruptcyInstruction<'keypair> { }; let liab_mint_info: MintInfo = account_loader.load(&self.liab_mint_info).await.unwrap(); - let liqee: MangoAccount = account_loader.load(&self.liqee).await.unwrap(); - let liqor: MangoAccount = account_loader.load(&self.liqor).await.unwrap(); + let liqee = account_loader + .load_mango_account(&self.liqee) + .await + .unwrap(); + let liqor = account_loader + .load_mango_account(&self.liqor) + .await + .unwrap(); let health_check_metas = derive_liquidation_remaining_account_metas( &account_loader, &liqee, @@ -2101,11 +2195,12 @@ impl<'keypair> ClientInstruction for LiqTokenBankruptcyInstruction<'keypair> { ) .await; - let group: Group = account_loader.load(&liqee.group).await.unwrap(); + let group_key = liqee.fixed.group; + let group: Group = account_loader.load(&group_key).await.unwrap(); let quote_mint_info = Pubkey::find_program_address( &[ - liqee.group.as_ref(), + liqee.fixed.group.as_ref(), b"MintInfo".as_ref(), group.insurance_mint.as_ref(), ], @@ -2115,13 +2210,13 @@ impl<'keypair> ClientInstruction for LiqTokenBankruptcyInstruction<'keypair> { let quote_mint_info: MintInfo = account_loader.load("e_mint_info).await.unwrap(); let insurance_vault = Pubkey::find_program_address( - &[liqee.group.as_ref(), b"InsuranceVault".as_ref()], + &[group_key.as_ref(), b"InsuranceVault".as_ref()], &program_id, ) .0; let accounts = Self::Accounts { - group: liqee.group, + group: group_key, liqee: self.liqee, liqor: self.liqor, liqor_owner: self.liqor_owner.pubkey(), @@ -2321,7 +2416,10 @@ impl<'keypair> ClientInstruction for PerpPlaceOrderInstruction<'keypair> { }; let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let health_check_metas = derive_health_check_remaining_account_metas( &account_loader, &account, @@ -2605,7 +2703,10 @@ impl ClientInstruction for ComputeAccountDataInstruction { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); let health_check_metas = derive_health_check_remaining_account_metas( &account_loader, @@ -2617,7 +2718,7 @@ impl ClientInstruction for ComputeAccountDataInstruction { .await; let accounts = Self::Accounts { - group: account.group, + group: account.fixed.group, account: self.account, }; diff --git a/programs/mango-v4/tests/test_bankrupt_tokens.rs b/programs/mango-v4/tests/test_bankrupt_tokens.rs index 7c13d24ba..e347629cb 100644 --- a/programs/mango-v4/tests/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/test_bankrupt_tokens.rs @@ -44,6 +44,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 2, + account_size: AccountSize::Large, group, owner, payer, @@ -89,6 +90,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -210,7 +212,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { account_position(solana, account, borrow_token1.bank).await, (-350.0f64 + (1000.0 / 20.0 / 1.04)).round() as i64 ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); @@ -230,13 +232,13 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { ) .await .unwrap(); - assert!(account_position_closed(solana, account, collateral_token2.bank).await,); + assert!(account_position_closed(solana, account, collateral_token2.bank).await); let borrow1_after_liq = -350.0f64 + (1000.0 / 20.0 / 1.04) + (20.0 / 20.0 / 1.04); assert_eq!( account_position(solana, account, borrow_token1.bank).await, borrow1_after_liq.round() as i64 ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(liqee.is_bankrupt()); @@ -262,7 +264,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { account_position(solana, vault_account, borrow_token1.bank).await, vault_before + (borrow1_after_liq.round() as i64) ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(liqee.is_bankrupt()); assert!(account_position_closed(solana, account, borrow_token1.bank).await); @@ -289,7 +291,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { account_position(solana, vault_account, borrow_token2.bank).await, (vault_amount - borrow2_amount) as i64 ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(!liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); assert!(account_position_closed(solana, account, borrow_token2.bank).await); @@ -351,6 +353,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 2, + account_size: AccountSize::Large, group, owner, payer, @@ -396,6 +399,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -513,7 +517,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { .await .unwrap(); assert!(account_position_closed(solana, account, collateral_token1.bank).await); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); @@ -534,7 +538,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { .await .unwrap(); assert!(account_position_closed(solana, account, collateral_token2.bank).await,); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(liqee.is_bankrupt()); @@ -559,7 +563,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { ) .await .unwrap(); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(liqee.is_bankrupt()); assert!(account_position_closed(solana, account, borrow_token1.bank).await); @@ -592,7 +596,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { ) .await .unwrap(); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(liqee.is_bankrupt()); assert!(account_position_closed(solana, account, borrow_token1.bank).await); @@ -627,7 +631,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { ) .await .unwrap(); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(!liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); assert!(account_position_closed(solana, account, borrow_token1.bank).await); diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index f25234435..c6b024888 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -40,6 +40,20 @@ async fn test_basic() -> Result<(), TransportError> { let account = send_tx( solana, AccountCreateInstruction { + account_num: 0, + account_size: AccountSize::Small, + group, + owner, + payer, + }, + ) + .await + .unwrap() + .account; + + send_tx( + solana, + AccountExpandInstruction { account_num: 0, group, owner, diff --git a/programs/mango-v4/tests/test_delegate.rs b/programs/mango-v4/tests/test_delegate.rs index 021505776..115bcc260 100644 --- a/programs/mango-v4/tests/test_delegate.rs +++ b/programs/mango-v4/tests/test_delegate.rs @@ -37,6 +37,7 @@ async fn test_delegate() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, diff --git a/programs/mango-v4/tests/test_group_address_lookup_tables.rs b/programs/mango-v4/tests/test_group_address_lookup_tables.rs index b09e09749..3748836b8 100644 --- a/programs/mango-v4/tests/test_group_address_lookup_tables.rs +++ b/programs/mango-v4/tests/test_group_address_lookup_tables.rs @@ -40,6 +40,7 @@ async fn test_group_address_lookup_tables() -> Result<()> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index 89dce2cdb..bd22f6131 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -37,6 +37,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -101,6 +102,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -212,6 +214,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index e89ca1d00..69a6d1da5 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -42,6 +42,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 2, + account_size: AccountSize::Large, group, owner, payer, @@ -97,6 +98,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -251,6 +253,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 2, + account_size: AccountSize::Large, group, owner, payer, @@ -281,6 +284,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -388,7 +392,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { -50 + 19 ); assert!(account_position_closed(solana, account, collateral_token2.bank).await,); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); @@ -418,7 +422,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { account_position(solana, account, collateral_token1.bank).await, 1000 - 32 ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); @@ -450,7 +454,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { account_position(solana, account, collateral_token1.bank).await, 1000 - 32 - 21 ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); @@ -485,7 +489,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { account_position(solana, account, collateral_token1.bank).await, 1000 - 32 - 535 - 1 ); - let liqee: MangoAccount = solana.get_account(account).await; + let liqee = get_mango_account(solana, account).await; assert!(!liqee.being_liquidated()); assert!(!liqee.is_bankrupt()); diff --git a/programs/mango-v4/tests/test_margin_trade.rs b/programs/mango-v4/tests/test_margin_trade.rs index 6ce1cbd83..02560e0b0 100644 --- a/programs/mango-v4/tests/test_margin_trade.rs +++ b/programs/mango-v4/tests/test_margin_trade.rs @@ -54,6 +54,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> { solana, AccountCreateInstruction { account_num: 1, + account_size: AccountSize::Large, group, owner, payer, @@ -96,6 +97,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -227,8 +229,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> { margin_account_initial + withdraw_amount ); // Check that position is fully deactivated - let account_data: MangoAccount = solana.get_account(account).await; - assert_eq!(account_data.tokens.iter_active().count(), 0); + let account_data = get_mango_account(solana, account).await; + assert_eq!(account_data.token_iter_active().count(), 0); // // TEST: Activating a token via margin trade @@ -370,6 +372,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> { solana, AccountCreateInstruction { account_num: 1, + account_size: AccountSize::Large, group, owner, payer, @@ -412,6 +415,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -540,8 +544,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> { margin_account_initial + withdraw_amount ); // Check that position is fully deactivated - let account_data: MangoAccount = solana.get_account(account).await; - assert_eq!(account_data.tokens.iter_active().count(), 0); + let account_data = get_mango_account(solana, account).await; + assert_eq!(account_data.token_iter_active().count(), 0); // // TEST: Activating a token via margin trade @@ -631,6 +635,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> { solana, AccountCreateInstruction { account_num: 1, + account_size: AccountSize::Large, group, owner, payer, @@ -673,6 +678,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -801,8 +807,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> { margin_account_initial + withdraw_amount ); // Check that position is fully deactivated - let account_data: MangoAccount = solana.get_account(account).await; - assert_eq!(account_data.tokens.iter_active().count(), 0); + let account_data = get_mango_account(solana, account).await; + assert_eq!(account_data.token_iter_active().count(), 0); // // TEST: Activating a token via margin trade diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index ce71c068f..d96d58440 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -36,6 +36,7 @@ async fn test_perp() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -49,6 +50,7 @@ async fn test_perp() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 1, + account_size: AccountSize::Large, group, owner, payer, @@ -200,8 +202,8 @@ async fn test_perp() -> Result<(), TransportError> { let order_id_to_cancel = solana .get_account::(account_0) .await - .perps - .order_id[0]; + .perp_open_orders[0] + .order_id; send_tx( solana, PerpCancelOrderInstruction { @@ -398,12 +400,12 @@ async fn test_perp() -> Result<(), TransportError> { .unwrap(); let mango_account_0 = solana.get_account::(account_0).await; - assert_eq!(mango_account_0.perps.accounts[0].base_position_lots, 1); - assert!(mango_account_0.perps.accounts[0].quote_position_native < -100.019); + assert_eq!(mango_account_0.perps[0].base_position_lots, 1); + assert!(mango_account_0.perps[0].quote_position_native < -100.019); let mango_account_1 = solana.get_account::(account_1).await; - assert_eq!(mango_account_1.perps.accounts[0].base_position_lots, -1); - assert_eq!(mango_account_1.perps.accounts[0].quote_position_native, 100); + assert_eq!(mango_account_1.perps[0].base_position_lots, -1); + assert_eq!(mango_account_1.perps[0].quote_position_native, 100); send_tx( solana, @@ -426,10 +428,10 @@ async fn test_perp() -> Result<(), TransportError> { async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) { let mango_account_0 = solana.get_account::(account_0).await; - for i in 0..MAX_PERP_OPEN_ORDERS { - assert!(mango_account_0.perps.order_id[i] == 0); - assert!(mango_account_0.perps.order_side[i] == Side::Bid); - assert!(mango_account_0.perps.client_order_id[i] == 0); - assert!(mango_account_0.perps.order_market[i] == FREE_ORDER_SLOT); + for oo in mango_account_0.perp_open_orders.iter() { + assert!(oo.order_id == 0); + assert!(oo.order_side == Side::Bid); + assert!(oo.client_order_id == 0); + assert!(oo.order_market == FREE_ORDER_SLOT); } } diff --git a/programs/mango-v4/tests/test_position_lifetime.rs b/programs/mango-v4/tests/test_position_lifetime.rs index 0f1be2629..4b341ec49 100644 --- a/programs/mango-v4/tests/test_position_lifetime.rs +++ b/programs/mango-v4/tests/test_position_lifetime.rs @@ -38,6 +38,7 @@ async fn test_position_lifetime() -> Result<()> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -51,6 +52,7 @@ async fn test_position_lifetime() -> Result<()> { solana, AccountCreateInstruction { account_num: 1, + account_size: AccountSize::Large, group, owner, payer, @@ -122,8 +124,8 @@ async fn test_position_lifetime() -> Result<()> { } // Check that positions are fully deactivated - let account: MangoAccount = solana.get_account(account).await; - assert_eq!(account.tokens.iter_active().count(), 0); + let account = get_mango_account(solana, account).await; + assert_eq!(account.token_iter_active().count(), 0); // No user tokens got lost for &payer_token in payer_mint_accounts { @@ -222,8 +224,8 @@ async fn test_position_lifetime() -> Result<()> { .unwrap(); // Check that positions are fully deactivated - let account: MangoAccount = solana.get_account(account).await; - assert_eq!(account.tokens.iter_active().count(), 0); + let account = get_mango_account(solana, account).await; + assert_eq!(account.token_iter_active().count(), 0); // No user tokens got lost // TODO: -1 is a workaround for rounding down in withdraw diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index e2c6d798a..0d8f914e4 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -40,6 +40,7 @@ async fn test_serum() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -126,11 +127,10 @@ async fn test_serum() -> Result<(), TransportError> { .unwrap() .open_orders; - let account_data: MangoAccount = solana.get_account(account).await; + let account_data = get_mango_account(solana, account).await; assert_eq!( account_data - .serum3 - .iter_active() + .serum3_iter_active() .map(|v| (v.open_orders, v.market_index)) .collect::>(), [(open_orders, 0)] diff --git a/programs/mango-v4/tests/test_token_update_index_and_rate.rs b/programs/mango-v4/tests/test_token_update_index_and_rate.rs index 02aa61001..f77dc4d48 100644 --- a/programs/mango-v4/tests/test_token_update_index_and_rate.rs +++ b/programs/mango-v4/tests/test_token_update_index_and_rate.rs @@ -36,6 +36,7 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 0, + account_size: AccountSize::Large, group, owner, payer, @@ -63,6 +64,7 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> { solana, AccountCreateInstruction { account_num: 1, + account_size: AccountSize::Large, group, owner, payer, diff --git a/programs/margin-trade/Cargo.toml b/programs/margin-trade/Cargo.toml index 5a74918ad..e585f6afe 100644 --- a/programs/margin-trade/Cargo.toml +++ b/programs/margin-trade/Cargo.toml @@ -18,6 +18,6 @@ default = [] test-bpf = [] [dependencies] -anchor-lang = { version = "0.25.0", features = [] } -anchor-spl = "0.25.0" +anchor-lang = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } +anchor-spl = { git = "https://github.com/blockworks-foundation/anchor.git", rev = "1153380487706e4d8f486071cf2f519468438eab" } solana-program = "1.10.29" diff --git a/release-to-devnet.sh b/release-to-devnet.sh index 16c34be42..16cfdbe63 100755 --- a/release-to-devnet.sh +++ b/release-to-devnet.sh @@ -2,12 +2,19 @@ set -e pipefail +ANCHOR_BRANCH=v0.25.0-mangov4 +ANCHOR_FORK=$(cd ../anchor && git rev-parse --abbrev-ref HEAD) +if [ "$ANCHOR_FORK" != "$ANCHOR_BRANCH" ]; then + echo "Check out anchor fork at git@github.com:blockworks-foundation/anchor.git, and switch to branch $ANCHOR_BRANCH!" + exit 1; +fi + WALLET_WITH_FUNDS=~/.config/solana/mango-devnet.json PROGRAM_ID=m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD # TODO fix need for --skip-lint # build program, -anchor build --skip-lint +cargo run --manifest-path ../anchor/cli/Cargo.toml build --skip-lint # patch types, which we want in rust, but anchor client doesn't support ./idl-fixup.sh @@ -23,12 +30,12 @@ if [[ -z "${NO_DEPLOY}" ]]; then -k $WALLET_WITH_FUNDS target/deploy/mango_v4.so --skip-fee-check # # publish idl - # anchor idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \ - # --filepath target/idl/mango_v4.json $PROGRAM_ID + cargo run --manifest-path ../anchor/cli/Cargo.toml idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \ + --filepath target/idl/mango_v4.json $PROGRAM_ID else echo "Skipping deployment..." fi -# # build npm package -# (cd ./ts/client && tsc) +# build npm package +(cd ./ts/client && tsc) diff --git a/release-to-mainnet.sh b/release-to-mainnet.sh index 21bd30650..f32479704 100755 --- a/release-to-mainnet.sh +++ b/release-to-mainnet.sh @@ -2,6 +2,13 @@ set -e pipefail +ANCHOR_BRANCH=v0.25.0-mangov4 +ANCHOR_FORK=$(cd ../anchor && git rev-parse --abbrev-ref HEAD) +if [ "$ANCHOR_FORK" != "$ANCHOR_BRANCH" ]; then + echo "Check out anchor fork at git@github.com:blockworks-foundation/anchor.git, and switch to branch $ANCHOR_BRANCH!" + exit 1; +fi + WALLET_WITH_FUNDS=~/.config/solana/mango-mainnet.json PROGRAM_ID=m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 482b8f2bf..2857dc4a0 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -23,6 +23,7 @@ export class MangoAccount { tokens: unknown; serum3: Object; perps: unknown; + perpOpenOrders: unknown; beingLiquidated: number; isBankrupt: number; accountNum: number; @@ -36,9 +37,10 @@ export class MangoAccount { obj.group, obj.owner, obj.delegate, - obj.tokens as { values: TokenPositionDto[] }, - obj.serum3 as { values: Serum3PositionDto[] }, - obj.perps as { accounts: PerpPositionDto[] }, + obj.tokens as TokenPositionDto[], + obj.serum3 as Serum3PositionDto[], + obj.perps as PerpPositionDto[], + obj.perpOpenOrders as any, // TODO obj.beingLiquidated, obj.isBankrupt, obj.accountNum, @@ -54,9 +56,10 @@ export class MangoAccount { public group: PublicKey, public owner: PublicKey, public delegate: PublicKey, - tokens: { values: TokenPositionDto[] }, - serum3: { values: Serum3PositionDto[] }, - perps: { accounts: PerpPositionDto[] }, + tokens: TokenPositionDto[], + serum3: Serum3PositionDto[], + perps: PerpPositionDto[], + perpOpenOrders: PerpPositionDto[], beingLiquidated: number, isBankrupt: number, accountNum: number, @@ -65,9 +68,9 @@ export class MangoAccount { public accountData: {}, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; - this.tokens = tokens.values.map((dto) => TokenPosition.from(dto)); - this.serum3 = serum3.values.map((dto) => Serum3Orders.from(dto)); - this.perps = perps.accounts.map((dto) => PerpPositions.from(dto)); + this.tokens = tokens.map((dto) => TokenPosition.from(dto)); + this.serum3 = serum3.map((dto) => Serum3Orders.from(dto)); + this.perps = perps.map((dto) => PerpPositions.from(dto)); } async reload(client: MangoClient, group: Group) { @@ -584,3 +587,8 @@ export class EquityDto { tokens: { tokenIndex: number; value: I80F48Dto }[]; perps: { perpMarketIndex: number; value: I80F48Dto }[]; } + +export class AccountSize { + static small = { small: {} }; + static large = { large: {} }; +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index b36cf4d08..163fb3432 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -30,7 +30,11 @@ import bs58 from 'bs58'; import { Bank, MintInfo } from './accounts/bank'; import { Group } from './accounts/group'; import { I80F48 } from './accounts/I80F48'; -import { MangoAccount, MangoAccountData } from './accounts/mangoAccount'; +import { + AccountSize, + MangoAccount, + MangoAccountData, +} from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; import { OrderType, PerpMarket, Side } from './accounts/perp'; import { @@ -424,11 +428,17 @@ export class MangoClient { group: Group, ownerPk: PublicKey, accountNumber?: number, + accountSize?: AccountSize, name?: string, ): Promise { let mangoAccounts = await this.getMangoAccountForOwner(group, ownerPk); if (mangoAccounts.length === 0) { - await this.createMangoAccount(group, accountNumber ?? 0, name ?? ''); + await this.createMangoAccount( + group, + accountNumber ?? 0, + accountSize ?? AccountSize.small, + name ?? '', + ); mangoAccounts = await this.getMangoAccountForOwner(group, ownerPk); } return mangoAccounts[0]; @@ -437,10 +447,11 @@ export class MangoClient { public async createMangoAccount( group: Group, accountNumber: number, + accountSize: AccountSize, name?: string, ): Promise { return await this.program.methods - .accountCreate(accountNumber, name ?? '') + .accountCreate(accountNumber, accountSize, name ?? '') .accounts({ group: group.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, @@ -449,6 +460,21 @@ export class MangoClient { .rpc(); } + public async expandMangoAccount( + group: Group, + account: MangoAccount, + ): Promise { + return await this.program.methods + .accountExpand() + .accounts({ + group: group.publicKey, + account: account.publicKey, + owner: (this.program.provider as AnchorProvider).wallet.publicKey, + payer: (this.program.provider as AnchorProvider).wallet.publicKey, + }) + .rpc(); + } + public async editMangoAccount( group: Group, mangoAccount: MangoAccount, @@ -462,7 +488,7 @@ export class MangoClient { account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, }) - .rpc(); + .rpc({ skipPreflight: true }); } public async getMangoAccount(mangoAccount: MangoAccount) { diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 0178e3950..34fccc4fd 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -646,12 +646,49 @@ export type MangoV4 = { "name": "accountNum", "type": "u8" }, + { + "name": "accountSize", + "type": { + "defined": "AccountSize" + } + }, { "name": "name", "type": "string" } ] }, + { + "name": "accountExpand", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "accountEdit", "accounts": [ @@ -1058,7 +1095,10 @@ export type MangoV4 = { { "name": "instructions", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] } ], "args": [ @@ -1093,6 +1133,11 @@ export type MangoV4 = { }, { "name": "serum3RegisterMarket", + "docs": [ + "", + "Serum", + "" + ], "accounts": [ { "name": "group", @@ -1393,7 +1438,10 @@ export type MangoV4 = { { "name": "marketVaultSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] }, { "name": "quoteBank", @@ -1539,7 +1587,7 @@ export type MangoV4 = { }, { "name": "account", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1641,7 +1689,10 @@ export type MangoV4 = { { "name": "marketVaultSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] }, { "name": "quoteBank", @@ -1867,6 +1918,11 @@ export type MangoV4 = { }, { "name": "perpCreateMarket", + "docs": [ + "", + "Perps", + "" + ], "accounts": [ { "name": "group", @@ -1910,7 +1966,11 @@ export type MangoV4 = { { "name": "bids", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "Accounts are initialised by client,", + "anchor discriminator is set first when ix exits," + ] }, { "name": "asks", @@ -2487,6 +2547,11 @@ export type MangoV4 = { }, { "name": "benchmark", + "docs": [ + "", + "benchmark", + "" + ], "accounts": [], "args": [] } @@ -2530,6 +2595,10 @@ export type MangoV4 = { }, { "name": "depositIndex", + "docs": [ + "the index used to scale the value of an IndexedPosition", + "TODO: should always be >= 0, add checks?" + ], "type": { "defined": "I80F48" } @@ -2542,6 +2611,11 @@ export type MangoV4 = { }, { "name": "cachedIndexedTotalDeposits", + "docs": [ + "total deposits/borrows, only updated during UpdateIndexAndRate", + "TODO: These values could be dropped from the bank, they're written in UpdateIndexAndRate", + "and never read." + ], "type": { "defined": "I80F48" } @@ -2554,6 +2628,17 @@ export type MangoV4 = { }, { "name": "indexedDeposits", + "docs": [ + "deposits/borrows for this bank", + "", + "Note that these may become negative. It's perfectly fine for users to borrow one one bank", + "(increasing indexed_borrows there) and paying back on another (possibly decreasing indexed_borrows", + "below zero).", + "", + "The vault amount is not deducable from these values.", + "", + "These become meaningful when summed over all banks (like in update_index_and_rate)." + ], "type": { "defined": "I80F48" } @@ -2789,30 +2874,18 @@ export type MangoV4 = { "name": "delegate", "type": "publicKey" }, - { - "name": "tokens", - "type": { - "defined": "MangoAccountTokenPositions" - } - }, - { - "name": "serum3", - "type": { - "defined": "MangoAccountSerum3Orders" - } - }, - { - "name": "perps", - "type": { - "defined": "MangoAccountPerpPositions" - } - }, { "name": "beingLiquidated", + "docs": [ + "This account cannot open new positions or borrow until `init_health >= 0`" + ], "type": "u8" }, { "name": "isBankrupt", + "docs": [ + "This account cannot do anything except go through `resolve_bankruptcy`" + ], "type": "u8" }, { @@ -2839,6 +2912,54 @@ export type MangoV4 = { { "name": "netSettled", "type": "f32" + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "tokens", + "type": { + "vec": { + "defined": "TokenPosition" + } + } + }, + { + "name": "padding2", + "type": "u32" + }, + { + "name": "serum3", + "type": { + "vec": { + "defined": "Serum3Orders" + } + } + }, + { + "name": "padding3", + "type": "u32" + }, + { + "name": "perps", + "type": { + "vec": { + "defined": "PerpPositions" + } + } + }, + { + "name": "padding4", + "type": "u32" + }, + { + "name": "perpOpenOrders", + "type": { + "vec": { + "defined": "PerpOpenOrders" + } + } } ] } @@ -2952,6 +3073,11 @@ export type MangoV4 = { }, { "name": "bookSide", + "docs": [ + "A binary tree on AnyNode::key()", + "", + "The key encodes the price in the top 64 bits." + ], "type": { "kind": "struct", "fields": [ @@ -3044,6 +3170,9 @@ export type MangoV4 = { }, { "name": "perpMarketIndex", + "docs": [ + "Lookup indices" + ], "type": "u16" }, { @@ -3088,10 +3217,18 @@ export type MangoV4 = { }, { "name": "quoteLotSize", + "docs": [ + "Number of quote native that reresents min tick" + ], "type": "i64" }, { "name": "baseLotSize", + "docs": [ + "Represents number of base native quantity", + "e.g. if base decimals for underlying asset are 6, base lot size is 100, and base position is 10000, then", + "UI position is 1" + ], "type": "i64" }, { @@ -3170,20 +3307,36 @@ export type MangoV4 = { }, { "name": "openInterest", + "docs": [ + "" + ], "type": "i64" }, { "name": "seqNum", + "docs": [ + "Total number of orders seen" + ], "type": "u64" }, { "name": "feesAccrued", + "docs": [ + "Fees accrued in native quote currency" + ], "type": { "defined": "I80F48" } }, { "name": "bump", + "docs": [ + "Liquidity mining metadata", + "pub liquidity_mining_info: LiquidityMiningInfo,", + "Token vault which holds mango tokens to be disbursed as liquidity incentives for this perp market", + "pub mngo_vault: Pubkey,", + "PDA bump" + ], "type": "u8" }, { @@ -3334,10 +3487,17 @@ export type MangoV4 = { "fields": [ { "name": "index", + "docs": [ + "Account index of the vault to withdraw from in the target_accounts section.", + "Index is counted after health accounts." + ], "type": "u8" }, { "name": "amount", + "docs": [ + "Requested withdraw amount." + ], "type": "u64" } ] @@ -3586,16 +3746,25 @@ export type MangoV4 = { "fields": [ { "name": "indexedPosition", + "docs": [ + "The deposit_index (if positive) or borrow_index (if negative) scaled position" + ], "type": { "defined": "I80F48" } }, { "name": "tokenIndex", + "docs": [ + "index into Group.tokens" + ], "type": "u16" }, { "name": "inUseCount", + "docs": [ + "incremented when a market requires this position to stay alive" + ], "type": "u8" }, { @@ -3610,25 +3779,6 @@ export type MangoV4 = { ] } }, - { - "name": "MangoAccountTokenPositions", - "type": { - "kind": "struct", - "fields": [ - { - "name": "values", - "type": { - "array": [ - { - "defined": "TokenPosition" - }, - 16 - ] - } - } - ] - } - }, { "name": "Serum3Orders", "type": { @@ -3652,6 +3802,11 @@ export type MangoV4 = { }, { "name": "baseTokenIndex", + "docs": [ + "Store the base/quote token index, so health computations don't need", + "to get passed the static SerumMarket to find which tokens a market", + "uses and look up the correct oracles." + ], "type": "u16" }, { @@ -3670,25 +3825,6 @@ export type MangoV4 = { ] } }, - { - "name": "MangoAccountSerum3Orders", - "type": { - "kind": "struct", - "fields": [ - { - "name": "values", - "type": { - "array": [ - { - "defined": "Serum3Orders" - }, - 8 - ] - } - } - ] - } - }, { "name": "PerpPositions", "type": { @@ -3709,16 +3845,26 @@ export type MangoV4 = { }, { "name": "basePositionLots", + "docs": [ + "Active position size, measured in base lots" + ], "type": "i64" }, { "name": "quotePositionNative", + "docs": [ + "Active position in quote (conversation rate is that of the time the order was settled)", + "measured in native quote" + ], "type": { "defined": "I80F48" } }, { "name": "longSettledFunding", + "docs": [ + "Already settled funding" + ], "type": { "defined": "I80F48" } @@ -3731,14 +3877,24 @@ export type MangoV4 = { }, { "name": "bidsBaseLots", + "docs": [ + "Base lots in bids" + ], "type": "i64" }, { "name": "asksBaseLots", + "docs": [ + "Base lots in asks" + ], "type": "i64" }, { "name": "takerBaseLots", + "docs": [ + "Liquidity mining rewards", + "Amount that's on EventQueue waiting to be processed" + ], "type": "i64" }, { @@ -3749,58 +3905,45 @@ export type MangoV4 = { } }, { - "name": "MangoAccountPerpPositions", + "name": "PerpOpenOrders", "type": { "kind": "struct", "fields": [ { - "name": "accounts", + "name": "orderSide", + "type": { + "defined": "Side" + } + }, + { + "name": "reserved1", "type": { "array": [ - { - "defined": "PerpPositions" - }, - 8 + "u8", + 1 ] } }, { "name": "orderMarket", - "type": { - "array": [ - "u16", - 8 - ] - } + "type": "u16" }, { - "name": "orderSide", + "name": "reserved2", "type": { "array": [ - { - "defined": "Side" - }, - 8 - ] - } - }, - { - "name": "orderId", - "type": { - "array": [ - "i128", - 8 + "u8", + 4 ] } }, { "name": "clientOrderId", - "type": { - "array": [ - "u64", - 8 - ] - } + "type": "u64" + }, + { + "name": "orderId", + "type": "i128" } ] } @@ -3883,6 +4026,10 @@ export type MangoV4 = { }, { "name": "TokenIndex", + "docs": [ + "Nothing in Rust shall use these types. They only exist so that the Anchor IDL", + "knows about them and typescript can deserialize it." + ], "type": { "kind": "struct", "fields": [ @@ -3931,6 +4078,9 @@ export type MangoV4 = { }, { "name": "Serum3SelfTradeBehavior", + "docs": [ + "Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them" + ], "type": { "kind": "enum", "variants": [ @@ -3979,6 +4129,14 @@ export type MangoV4 = { }, { "name": "HealthType", + "docs": [ + "There are two types of health, initial health used for opening new positions and maintenance", + "health used for liquidations. They are both calculated as a weighted sum of the assets", + "minus the liabilities but the maint. health uses slightly larger weights for assets and", + "slightly smaller weights for the liabilities. Zero is used as the bright line for both", + "i.e. if your init health falls below zero, you cannot open new positions and if your maint. health", + "falls below zero you will be liquidated." + ], "type": { "kind": "enum", "variants": [ @@ -3991,6 +4149,20 @@ export type MangoV4 = { ] } }, + { + "name": "AccountSize", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Small" + }, + { + "name": "Large" + } + ] + } + }, { "name": "OracleType", "type": { @@ -5345,12 +5517,49 @@ export const IDL: MangoV4 = { "name": "accountNum", "type": "u8" }, + { + "name": "accountSize", + "type": { + "defined": "AccountSize" + } + }, { "name": "name", "type": "string" } ] }, + { + "name": "accountExpand", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "accountEdit", "accounts": [ @@ -5757,7 +5966,10 @@ export const IDL: MangoV4 = { { "name": "instructions", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] } ], "args": [ @@ -5792,6 +6004,11 @@ export const IDL: MangoV4 = { }, { "name": "serum3RegisterMarket", + "docs": [ + "", + "Serum", + "" + ], "accounts": [ { "name": "group", @@ -6092,7 +6309,10 @@ export const IDL: MangoV4 = { { "name": "marketVaultSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] }, { "name": "quoteBank", @@ -6238,7 +6458,7 @@ export const IDL: MangoV4 = { }, { "name": "account", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -6340,7 +6560,10 @@ export const IDL: MangoV4 = { { "name": "marketVaultSigner", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] }, { "name": "quoteBank", @@ -6566,6 +6789,11 @@ export const IDL: MangoV4 = { }, { "name": "perpCreateMarket", + "docs": [ + "", + "Perps", + "" + ], "accounts": [ { "name": "group", @@ -6609,7 +6837,11 @@ export const IDL: MangoV4 = { { "name": "bids", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "Accounts are initialised by client,", + "anchor discriminator is set first when ix exits," + ] }, { "name": "asks", @@ -7186,6 +7418,11 @@ export const IDL: MangoV4 = { }, { "name": "benchmark", + "docs": [ + "", + "benchmark", + "" + ], "accounts": [], "args": [] } @@ -7229,6 +7466,10 @@ export const IDL: MangoV4 = { }, { "name": "depositIndex", + "docs": [ + "the index used to scale the value of an IndexedPosition", + "TODO: should always be >= 0, add checks?" + ], "type": { "defined": "I80F48" } @@ -7241,6 +7482,11 @@ export const IDL: MangoV4 = { }, { "name": "cachedIndexedTotalDeposits", + "docs": [ + "total deposits/borrows, only updated during UpdateIndexAndRate", + "TODO: These values could be dropped from the bank, they're written in UpdateIndexAndRate", + "and never read." + ], "type": { "defined": "I80F48" } @@ -7253,6 +7499,17 @@ export const IDL: MangoV4 = { }, { "name": "indexedDeposits", + "docs": [ + "deposits/borrows for this bank", + "", + "Note that these may become negative. It's perfectly fine for users to borrow one one bank", + "(increasing indexed_borrows there) and paying back on another (possibly decreasing indexed_borrows", + "below zero).", + "", + "The vault amount is not deducable from these values.", + "", + "These become meaningful when summed over all banks (like in update_index_and_rate)." + ], "type": { "defined": "I80F48" } @@ -7488,30 +7745,18 @@ export const IDL: MangoV4 = { "name": "delegate", "type": "publicKey" }, - { - "name": "tokens", - "type": { - "defined": "MangoAccountTokenPositions" - } - }, - { - "name": "serum3", - "type": { - "defined": "MangoAccountSerum3Orders" - } - }, - { - "name": "perps", - "type": { - "defined": "MangoAccountPerpPositions" - } - }, { "name": "beingLiquidated", + "docs": [ + "This account cannot open new positions or borrow until `init_health >= 0`" + ], "type": "u8" }, { "name": "isBankrupt", + "docs": [ + "This account cannot do anything except go through `resolve_bankruptcy`" + ], "type": "u8" }, { @@ -7538,6 +7783,54 @@ export const IDL: MangoV4 = { { "name": "netSettled", "type": "f32" + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "tokens", + "type": { + "vec": { + "defined": "TokenPosition" + } + } + }, + { + "name": "padding2", + "type": "u32" + }, + { + "name": "serum3", + "type": { + "vec": { + "defined": "Serum3Orders" + } + } + }, + { + "name": "padding3", + "type": "u32" + }, + { + "name": "perps", + "type": { + "vec": { + "defined": "PerpPositions" + } + } + }, + { + "name": "padding4", + "type": "u32" + }, + { + "name": "perpOpenOrders", + "type": { + "vec": { + "defined": "PerpOpenOrders" + } + } } ] } @@ -7651,6 +7944,11 @@ export const IDL: MangoV4 = { }, { "name": "bookSide", + "docs": [ + "A binary tree on AnyNode::key()", + "", + "The key encodes the price in the top 64 bits." + ], "type": { "kind": "struct", "fields": [ @@ -7743,6 +8041,9 @@ export const IDL: MangoV4 = { }, { "name": "perpMarketIndex", + "docs": [ + "Lookup indices" + ], "type": "u16" }, { @@ -7787,10 +8088,18 @@ export const IDL: MangoV4 = { }, { "name": "quoteLotSize", + "docs": [ + "Number of quote native that reresents min tick" + ], "type": "i64" }, { "name": "baseLotSize", + "docs": [ + "Represents number of base native quantity", + "e.g. if base decimals for underlying asset are 6, base lot size is 100, and base position is 10000, then", + "UI position is 1" + ], "type": "i64" }, { @@ -7869,20 +8178,36 @@ export const IDL: MangoV4 = { }, { "name": "openInterest", + "docs": [ + "" + ], "type": "i64" }, { "name": "seqNum", + "docs": [ + "Total number of orders seen" + ], "type": "u64" }, { "name": "feesAccrued", + "docs": [ + "Fees accrued in native quote currency" + ], "type": { "defined": "I80F48" } }, { "name": "bump", + "docs": [ + "Liquidity mining metadata", + "pub liquidity_mining_info: LiquidityMiningInfo,", + "Token vault which holds mango tokens to be disbursed as liquidity incentives for this perp market", + "pub mngo_vault: Pubkey,", + "PDA bump" + ], "type": "u8" }, { @@ -8033,10 +8358,17 @@ export const IDL: MangoV4 = { "fields": [ { "name": "index", + "docs": [ + "Account index of the vault to withdraw from in the target_accounts section.", + "Index is counted after health accounts." + ], "type": "u8" }, { "name": "amount", + "docs": [ + "Requested withdraw amount." + ], "type": "u64" } ] @@ -8285,16 +8617,25 @@ export const IDL: MangoV4 = { "fields": [ { "name": "indexedPosition", + "docs": [ + "The deposit_index (if positive) or borrow_index (if negative) scaled position" + ], "type": { "defined": "I80F48" } }, { "name": "tokenIndex", + "docs": [ + "index into Group.tokens" + ], "type": "u16" }, { "name": "inUseCount", + "docs": [ + "incremented when a market requires this position to stay alive" + ], "type": "u8" }, { @@ -8309,25 +8650,6 @@ export const IDL: MangoV4 = { ] } }, - { - "name": "MangoAccountTokenPositions", - "type": { - "kind": "struct", - "fields": [ - { - "name": "values", - "type": { - "array": [ - { - "defined": "TokenPosition" - }, - 16 - ] - } - } - ] - } - }, { "name": "Serum3Orders", "type": { @@ -8351,6 +8673,11 @@ export const IDL: MangoV4 = { }, { "name": "baseTokenIndex", + "docs": [ + "Store the base/quote token index, so health computations don't need", + "to get passed the static SerumMarket to find which tokens a market", + "uses and look up the correct oracles." + ], "type": "u16" }, { @@ -8369,25 +8696,6 @@ export const IDL: MangoV4 = { ] } }, - { - "name": "MangoAccountSerum3Orders", - "type": { - "kind": "struct", - "fields": [ - { - "name": "values", - "type": { - "array": [ - { - "defined": "Serum3Orders" - }, - 8 - ] - } - } - ] - } - }, { "name": "PerpPositions", "type": { @@ -8408,16 +8716,26 @@ export const IDL: MangoV4 = { }, { "name": "basePositionLots", + "docs": [ + "Active position size, measured in base lots" + ], "type": "i64" }, { "name": "quotePositionNative", + "docs": [ + "Active position in quote (conversation rate is that of the time the order was settled)", + "measured in native quote" + ], "type": { "defined": "I80F48" } }, { "name": "longSettledFunding", + "docs": [ + "Already settled funding" + ], "type": { "defined": "I80F48" } @@ -8430,14 +8748,24 @@ export const IDL: MangoV4 = { }, { "name": "bidsBaseLots", + "docs": [ + "Base lots in bids" + ], "type": "i64" }, { "name": "asksBaseLots", + "docs": [ + "Base lots in asks" + ], "type": "i64" }, { "name": "takerBaseLots", + "docs": [ + "Liquidity mining rewards", + "Amount that's on EventQueue waiting to be processed" + ], "type": "i64" }, { @@ -8448,58 +8776,45 @@ export const IDL: MangoV4 = { } }, { - "name": "MangoAccountPerpPositions", + "name": "PerpOpenOrders", "type": { "kind": "struct", "fields": [ { - "name": "accounts", + "name": "orderSide", + "type": { + "defined": "Side" + } + }, + { + "name": "reserved1", "type": { "array": [ - { - "defined": "PerpPositions" - }, - 8 + "u8", + 1 ] } }, { "name": "orderMarket", - "type": { - "array": [ - "u16", - 8 - ] - } + "type": "u16" }, { - "name": "orderSide", + "name": "reserved2", "type": { "array": [ - { - "defined": "Side" - }, - 8 - ] - } - }, - { - "name": "orderId", - "type": { - "array": [ - "i128", - 8 + "u8", + 4 ] } }, { "name": "clientOrderId", - "type": { - "array": [ - "u64", - 8 - ] - } + "type": "u64" + }, + { + "name": "orderId", + "type": "i128" } ] } @@ -8582,6 +8897,10 @@ export const IDL: MangoV4 = { }, { "name": "TokenIndex", + "docs": [ + "Nothing in Rust shall use these types. They only exist so that the Anchor IDL", + "knows about them and typescript can deserialize it." + ], "type": { "kind": "struct", "fields": [ @@ -8630,6 +8949,9 @@ export const IDL: MangoV4 = { }, { "name": "Serum3SelfTradeBehavior", + "docs": [ + "Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them" + ], "type": { "kind": "enum", "variants": [ @@ -8678,6 +9000,14 @@ export const IDL: MangoV4 = { }, { "name": "HealthType", + "docs": [ + "There are two types of health, initial health used for opening new positions and maintenance", + "health used for liquidations. They are both calculated as a weighted sum of the assets", + "minus the liabilities but the maint. health uses slightly larger weights for assets and", + "slightly smaller weights for the liabilities. Zero is used as the bright line for both", + "i.e. if your init health falls below zero, you cannot open new positions and if your maint. health", + "falls below zero you will be liquidated." + ], "type": { "kind": "enum", "variants": [ @@ -8690,6 +9020,20 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "AccountSize", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Small" + }, + { + "name": "Large" + } + ] + } + }, { "name": "OracleType", "type": { diff --git a/ts/client/src/scripts/example1-flash-loan.ts b/ts/client/src/scripts/example1-flash-loan.ts index 1bf076b29..7e9c98c3f 100644 --- a/ts/client/src/scripts/example1-flash-loan.ts +++ b/ts/client/src/scripts/example1-flash-loan.ts @@ -1,6 +1,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair } from '@solana/web3.js'; import fs from 'fs'; +import { AccountSize } from '../accounts/mangoAccount'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; @@ -44,6 +45,7 @@ async function main() { group, user.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); diff --git a/ts/client/src/scripts/example1-user.ts b/ts/client/src/scripts/example1-user.ts index 7751f950b..ba6e7ea82 100644 --- a/ts/client/src/scripts/example1-user.ts +++ b/ts/client/src/scripts/example1-user.ts @@ -1,7 +1,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; -import { HealthType } from '../accounts/mangoAccount'; +import { AccountSize, HealthType } from '../accounts/mangoAccount'; import { OrderType, Side } from '../accounts/perp'; import { Serum3OrderType, @@ -59,6 +59,7 @@ async function main() { group, user.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); @@ -92,13 +93,18 @@ async function main() { if (true) { // deposit and withdraw - console.log(`...depositing 50 USDC`); - await client.tokenDeposit(group, mangoAccount, 'USDC', 50); - await mangoAccount.reload(client, group); - console.log(`...depositing 0.0005 BTC`); - await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005); - await mangoAccount.reload(client, group); + try { + console.log(`...depositing 50 USDC`); + await client.tokenDeposit(group, mangoAccount, 'USDC', 50); + await mangoAccount.reload(client, group); + + console.log(`...depositing 0.0005 BTC`); + await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005); + await mangoAccount.reload(client, group); + } catch (error) { + console.log(error); + } // witdrawing fails if no (other) user has deposited ORCA in the group // console.log(`Withdrawing...0.1 ORCA`); diff --git a/ts/client/src/scripts/mb-example1-ids-json.ts b/ts/client/src/scripts/mb-example1-ids-json.ts index 58b17ae4b..10dbf0035 100644 --- a/ts/client/src/scripts/mb-example1-ids-json.ts +++ b/ts/client/src/scripts/mb-example1-ids-json.ts @@ -46,6 +46,7 @@ async function main() { group, user.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); diff --git a/ts/client/src/scripts/mb-flash-loan-3.ts b/ts/client/src/scripts/mb-flash-loan-3.ts index 69fef2949..571a5b127 100644 --- a/ts/client/src/scripts/mb-flash-loan-3.ts +++ b/ts/client/src/scripts/mb-flash-loan-3.ts @@ -61,6 +61,7 @@ async function main() { group, user.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); diff --git a/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts b/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts index 36d795624..6ae5f8200 100644 --- a/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts +++ b/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts @@ -40,6 +40,7 @@ async function main() { group, user1.publicKey, 0, + AccountSize.small, 'my_mango_account', ); @@ -72,6 +73,7 @@ async function main() { group, user2.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...mangoAccount2 ${user2MangoAccount.publicKey}`); diff --git a/ts/client/src/scripts/scratch/example1-ob.ts b/ts/client/src/scripts/scratch/example1-ob.ts index 5a57faf4d..60befaba7 100644 --- a/ts/client/src/scripts/scratch/example1-ob.ts +++ b/ts/client/src/scripts/scratch/example1-ob.ts @@ -46,6 +46,7 @@ async function main() { group, user.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); diff --git a/ts/client/src/scripts/scratch/example1-user-account.ts b/ts/client/src/scripts/scratch/example1-user-account.ts index 4fd4950ca..633fb5907 100644 --- a/ts/client/src/scripts/scratch/example1-user-account.ts +++ b/ts/client/src/scripts/scratch/example1-user-account.ts @@ -47,6 +47,7 @@ async function main() { group, user.publicKey, 0, + AccountSize.small, 'my_mango_account', ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); diff --git a/update-local-idl.sh b/update-local-idl.sh index 941624d15..a634f6588 100755 --- a/update-local-idl.sh +++ b/update-local-idl.sh @@ -2,9 +2,16 @@ set -e pipefail +ANCHOR_BRANCH=v0.25.0-mangov4 +ANCHOR_FORK=$(cd ../anchor && git rev-parse --abbrev-ref HEAD) +if [ "$ANCHOR_FORK" != "$ANCHOR_BRANCH" ]; then + echo "Check out anchor fork at git@github.com:blockworks-foundation/anchor.git, and switch to branch $ANCHOR_BRANCH!" + exit 1; +fi + # TODO fix need for --skip-lint # build program, -anchor build --skip-lint +cargo run --manifest-path ../anchor/cli/Cargo.toml build --skip-lint # patch types, which we want in rust, but anchor client doesn't support ./idl-fixup.sh