From 6cef8caa34e613484a55fdce0a7308093ea79615 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 4 Jan 2023 16:15:46 +0100 Subject: [PATCH 1/5] rust client: Make AccountFetcher futures Send (#357) This required redoing the cached account fetcher logic to properly deal with locking and repeated calls while a fetch is ongoing. --- .github/workflows/ci-code-review-rust.yml | 5 +- Cargo.lock | 7 + client/Cargo.toml | 1 + client/src/account_fetcher.rs | 159 +++++++++++++++++----- client/src/chain_data_fetcher.rs | 2 +- client/src/client.rs | 4 +- 6 files changed, 140 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci-code-review-rust.yml b/.github/workflows/ci-code-review-rust.yml index 33344254c..ac59379a1 100644 --- a/.github/workflows/ci-code-review-rust.yml +++ b/.github/workflows/ci-code-review-rust.yml @@ -22,7 +22,7 @@ on: env: CARGO_TERM_COLOR: always SOLANA_VERSION: '1.14.9' - RUST_TOOLCHAIN: '1.60.0' + RUST_TOOLCHAIN: '1.65.0' LOG_PROGRAM: 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD' defaults: @@ -59,7 +59,8 @@ jobs: run: cargo fmt -- --check - name: Run clippy - run: cargo clippy -- --deny=warnings --allow=clippy::style --allow=clippy::complexity + # The --allow args are due to clippy scanning anchor + run: cargo clippy --no-deps -- --deny=warnings --allow=clippy::style --allow=clippy::complexity --allow=clippy::manual-retain --allow=clippy::crate-in-macro-def --allow=clippy::result-large-err tests: name: Run tests diff --git a/Cargo.lock b/Cargo.lock index c81d48db7..5edfe6dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,12 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-once-cell" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61305cacf1d0c5c9d3ee283d22f8f1f8c743a18ceb44a1b102bd53476c141de" + [[package]] name = "async-stream" version = "0.2.1" @@ -1118,6 +1124,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "anyhow", + "async-once-cell", "async-trait", "base64 0.13.1", "bincode", diff --git a/client/Cargo.toml b/client/Cargo.toml index a9c1274b8..ffda6c0d2 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,6 +11,7 @@ anchor-client = { path = "../anchor/client" } anchor-lang = { path = "../anchor/lang" } anchor-spl = { path = "../anchor/spl" } anyhow = "1.0" +async-once-cell = { version = "0.4.2", features = ["unpin"] } async-trait = "0.1.52" fixed = { version = "=1.11.0", features = ["serde", "borsh"] } fixed-macro = "^1.1.1" diff --git a/client/src/account_fetcher.rs b/client/src/account_fetcher.rs index e29b73dc6..6c1273a0f 100644 --- a/client/src/account_fetcher.rs +++ b/client/src/account_fetcher.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; +use std::sync::Arc; use std::sync::Mutex; +use async_once_cell::unpin::Lazy; + use anyhow::Context; use anchor_client::ClientError; @@ -12,7 +15,7 @@ use solana_sdk::pubkey::Pubkey; use mango_v4::state::MangoAccountValue; -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] pub trait AccountFetcher: Sync + Send { async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result; async fn fetch_raw_account_lookup_table( @@ -54,7 +57,7 @@ pub struct RpcAccountFetcher { pub rpc: RpcClientAsync, } -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl AccountFetcher for RpcAccountFetcher { async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { self.rpc @@ -99,9 +102,44 @@ impl AccountFetcher for RpcAccountFetcher { } } +struct CoalescedAsyncJob { + jobs: HashMap>>, +} + +impl Default for CoalescedAsyncJob { + fn default() -> Self { + Self { + jobs: Default::default(), + } + } +} + +impl CoalescedAsyncJob { + /// Either returns the job for `key` or registers a new job for it + fn run_coalesced + Send + 'static>( + &mut self, + key: Key, + fut: F, + ) -> Arc> { + self.jobs + .entry(key) + .or_insert_with(|| Arc::new(Lazy::new(Box::pin(fut)))) + .clone() + } + + fn remove(&mut self, key: &Key) { + self.jobs.remove(key); + } +} + +#[derive(Default)] struct AccountCache { accounts: HashMap, keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec>, + + account_jobs: CoalescedAsyncJob>, + program_accounts_jobs: + CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result>>, } impl AccountCache { @@ -112,18 +150,24 @@ impl AccountCache { } pub struct CachedAccountFetcher { - fetcher: T, - cache: Mutex, + fetcher: Arc, + cache: Arc>, +} + +impl Clone for CachedAccountFetcher { + fn clone(&self) -> Self { + Self { + fetcher: self.fetcher.clone(), + cache: self.cache.clone(), + } + } } impl CachedAccountFetcher { - pub fn new(fetcher: T) -> Self { + pub fn new(fetcher: Arc) -> Self { Self { fetcher, - cache: Mutex::new(AccountCache { - accounts: HashMap::new(), - keys_for_program_and_discriminator: HashMap::new(), - }), + cache: Arc::new(Mutex::new(AccountCache::default())), } } @@ -133,16 +177,41 @@ impl CachedAccountFetcher { } } -#[async_trait::async_trait(?Send)] -impl AccountFetcher for CachedAccountFetcher { +#[async_trait::async_trait] +impl AccountFetcher for CachedAccountFetcher { async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result { - let mut cache = self.cache.lock().unwrap(); - if let Some(account) = cache.accounts.get(address) { - return Ok(account.clone()); + let fetch_job = { + let mut cache = self.cache.lock().unwrap(); + if let Some(acc) = cache.accounts.get(address) { + return Ok(acc.clone()); + } + + // Start or fetch a reference to the fetch + cache update job + let self_copy = self.clone(); + let address_copy = address.clone(); + cache.account_jobs.run_coalesced(*address, async move { + let result = self_copy.fetcher.fetch_raw_account(&address_copy).await; + let mut cache = self_copy.cache.lock().unwrap(); + + // remove the job from the job list, so it can be redone if it errored + cache.account_jobs.remove(&address_copy); + + // store a successful fetch + if let Ok(account) = result.as_ref() { + cache.accounts.insert(address_copy, account.clone()); + } + result + }) + }; + + match fetch_job.get().await { + Ok(v) => Ok(v.clone()), + // Can't clone the stored error, so need to stringize it + Err(err) => Err(anyhow::format_err!( + "fetch error in CachedAccountFetcher: {:?}", + err + )), } - let account = self.fetcher.fetch_raw_account(address).await?; - cache.accounts.insert(*address, account.clone()); - Ok(account) } async fn fetch_program_accounts( @@ -151,23 +220,45 @@ impl AccountFetcher for CachedAccountFetcher { discriminator: [u8; 8], ) -> anyhow::Result> { let cache_key = (*program, discriminator); - let mut cache = self.cache.lock().unwrap(); - if let Some(accounts) = cache.keys_for_program_and_discriminator.get(&cache_key) { - return Ok(accounts - .iter() - .map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone())) - .collect::>()); + let fetch_job = { + let mut cache = self.cache.lock().unwrap(); + if let Some(accounts) = cache.keys_for_program_and_discriminator.get(&cache_key) { + return Ok(accounts + .iter() + .map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone())) + .collect::>()); + } + + let self_copy = self.clone(); + let program_copy = program.clone(); + cache + .program_accounts_jobs + .run_coalesced(cache_key.clone(), async move { + let result = self_copy + .fetcher + .fetch_program_accounts(&program_copy, discriminator) + .await; + let mut cache = self_copy.cache.lock().unwrap(); + cache.program_accounts_jobs.remove(&cache_key); + if let Ok(accounts) = result.as_ref() { + cache + .keys_for_program_and_discriminator + .insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect()); + for (pk, acc) in accounts.iter() { + cache.accounts.insert(*pk, acc.clone()); + } + } + result + }) + }; + + match fetch_job.get().await { + Ok(v) => Ok(v.clone()), + // Can't clone the stored error, so need to stringize it + Err(err) => Err(anyhow::format_err!( + "fetch error in CachedAccountFetcher: {:?}", + err + )), } - let accounts = self - .fetcher - .fetch_program_accounts(program, discriminator) - .await?; - cache - .keys_for_program_and_discriminator - .insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect()); - for (pk, acc) in accounts.iter() { - cache.accounts.insert(*pk, acc.clone()); - } - Ok(accounts) } } diff --git a/client/src/chain_data_fetcher.rs b/client/src/chain_data_fetcher.rs index 090c9025a..c6058f3a7 100644 --- a/client/src/chain_data_fetcher.rs +++ b/client/src/chain_data_fetcher.rs @@ -141,7 +141,7 @@ impl AccountFetcher { } } -#[async_trait::async_trait(?Send)] +#[async_trait::async_trait] impl crate::AccountFetcher for AccountFetcher { async fn fetch_raw_account( &self, diff --git a/client/src/client.rs b/client/src/client.rs index e65277bc3..7630f21e9 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -220,7 +220,9 @@ impl MangoClient { owner: Keypair, ) -> anyhow::Result { let rpc = client.rpc_async(); - let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc })); + let account_fetcher = Arc::new(CachedAccountFetcher::new(Arc::new(RpcAccountFetcher { + rpc, + }))); let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, &account).await?; let group = mango_account.fixed.group; From 5019864b844ca6633ee7d1356c763d78fc52ad64 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Wed, 4 Jan 2023 16:54:28 +0100 Subject: [PATCH 2/5] Update script to include perp market to reduce only Signed-off-by: microwavedcola1 --- ts/client/src/scripts/mb-admin.ts | 72 +++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/ts/client/src/scripts/mb-admin.ts b/ts/client/src/scripts/mb-admin.ts index 3904fb34a..105f5b15b 100644 --- a/ts/client/src/scripts/mb-admin.ts +++ b/ts/client/src/scripts/mb-admin.ts @@ -1,4 +1,9 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + NATIVE_MINT, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { AddressLookupTableProgram, ComputeBudgetProgram, @@ -20,11 +25,6 @@ import { import { MangoClient } from '../client'; import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID } from '../constants'; import { buildVersionedTx, toNative } from '../utils'; -import { - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - NATIVE_MINT, -} from '@solana/spl-token'; const GROUP_NUM = Number(process.env.GROUP_NUM || 0); @@ -77,7 +77,7 @@ const defaultInterestRate = { maxRate: 2.0, }; -async function buildAdminClient(): Promise<[MangoClient, Keypair]> { +async function buildAdminClient(): Promise<[MangoClient, Keypair, Keypair]> { const admin = Keypair.fromSecretKey( Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER2_KEYPAIR!, 'utf-8'))), ); @@ -98,7 +98,11 @@ async function buildAdminClient(): Promise<[MangoClient, Keypair]> { }, ); - return [client, admin]; + const creator = Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))), + ); + + return [client, admin, creator]; } async function buildUserClient( @@ -143,8 +147,9 @@ async function changeAdmin() { const result = await buildAdminClient(); const client = result[0]; const admin = result[1]; + const creator = result[2]; - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); console.log(`Changing admin...`); await client.groupEdit( @@ -157,8 +162,9 @@ async function registerTokens() { const result = await buildAdminClient(); const client = result[0]; const admin = result[1]; + const creator = result[2]; - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); console.log(`Creating USDC stub oracle...`); const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!); @@ -329,8 +335,9 @@ async function registerSerum3Markets() { const result = await buildAdminClient(); const client = result[0]; const admin = result[1]; + const creator = result[2]; - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); // Register SOL serum market await client.serum3RegisterMarket( @@ -384,8 +391,9 @@ async function registerPerpMarkets() { const result = await buildAdminClient(); const client = result[0]; const admin = result[1]; + const creator = result[2]; - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); await client.perpCreateMarket( group, @@ -448,6 +456,45 @@ async function registerPerpMarkets() { ); } +async function makePerpMarketReduceOnly() { + const result = await buildAdminClient(); + const client = result[0]; + const admin = result[1]; + const creator = result[2]; + + const group = await client.getGroupForCreator(creator.publicKey, GROUP_NUM); + const perpMarket = group.getPerpMarketByName('MNGO-PERP'); + await client.perpEditMarket( + group, + perpMarket.perpMarketIndex, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + true, + ); +} + async function createAndPopulateAlt() { const result = await buildAdminClient(); const client = result[0]; @@ -607,6 +654,7 @@ async function main() { try { // await registerPerpMarkets(); + // await makePerpMarketReduceOnly(); } catch (error) { console.log(error); } @@ -618,7 +666,7 @@ async function main() { } try { - createAndPopulateAlt(); + // createAndPopulateAlt(); } catch (error) {} } From 15784ecd2be97d17c50f297b7b589f71ad4145f0 Mon Sep 17 00:00:00 2001 From: silas <95582913+silas-x@users.noreply.github.com> Date: Wed, 4 Jan 2023 18:13:29 +0000 Subject: [PATCH 3/5] add security policy and on-chain metadata (#361) * add security-txt macro * minor fmt fix --- Cargo.lock | 19 +++++++++++++++++++ SECURITY.md | 31 +++++++++++++++++++++++++++++++ programs/mango-v4/Cargo.toml | 2 ++ programs/mango-v4/src/lib.rs | 14 ++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 SECURITY.md diff --git a/Cargo.lock b/Cargo.lock index 5edfe6dc8..377c7f76a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1452,6 +1452,17 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +[[package]] +name = "default-env" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f753eb82d29277e79efc625e84aecacfd4851ee50e05a8573a4740239a77bfd3" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "der" version = "0.5.1" @@ -3121,6 +3132,7 @@ dependencies = [ "borsh", "bytemuck", "checked_math", + "default-env", "derivative", "env_logger 0.9.3", "fixed", @@ -3139,6 +3151,7 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", + "solana-security-txt", "spl-associated-token-account", "spl-token", "static_assertions", @@ -6214,6 +6227,12 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "solana-security-txt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0461f3afb29d8591300b3dd09b5472b3772d65688a2826ad960b8c0d5fa605" + [[package]] name = "solana-send-transaction-service" version = "1.14.10" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..7d1b8a132 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Important Notice +Please **DO NOT** create a GitHub issue to report a security problem. Instead, please send an email to hello@blockworks.foundation with a detailed description of the attack vector and security risk you have identified. + +# Bug Bounty Overview +Mango Markets offers bug bounties for Mango Markets' on-chain program code; UI only bugs are omitted. + +|Severity|Description|Bug Bounty| +|-----------|--------------|-------------| +|Critical|Bugs that freeze user funds or drain the contract's holdings or involve theft of funds without user signatures|10% of the value of the hack up to $1,000,000| +|High|Bugs that could temporarily freeze user funds or incorrectly assign value to user funds|$10,000 to $50,000 per bug, assessed on a case by case basis| +|Medium/Low|Bugs that don't threaten user funds|$1,000 to $5,000 per bug, assessed on a case by case basis| + +The severity guidelines are based on [Immunefi's classification system](https://immunefi.com/severity-updated/). +Note that these are simply guidelines for the severity of the bugs. Each bug bounty submission will be evaluated on a case-by-case basis. + +## Submission +Please email hello@blockworks.foundation with a detailed description of the attack vector. For critical and moderate bugs, we require a proof of concept done on a privately deployed mainnet contract. We will reach out in 1 business day with additional questions or next steps on the bug bounty. + +## Bug Bounty Payment +Bug bounties will be paid in USDC or locked MNGO, after a DAO vote. The Mango DAO has never refused a valid bug bounty so far. + +## Invalid Bug Bounties +The following are out of scope for the bug bounty: +1. Attacks that the reporter has already exploited themselves, leading to damage. +2. Attacks requiring access to leaked keys/credentials. +3. Attacks requiring access to privileged addresses (governance, admin). +4. Incorrect data supplied by third party oracles (this does not exclude oracle manipulation/flash loan attacks). +5. Lack of liquidity. +6. Third party, off-chain bot errors (for instance bugs with an arbitrage bot running on the smart contracts). +7. Best practice critiques. +8. Sybil attacks. \ No newline at end of file diff --git a/programs/mango-v4/Cargo.toml b/programs/mango-v4/Cargo.toml index 5dccb6fc0..afe893f93 100644 --- a/programs/mango-v4/Cargo.toml +++ b/programs/mango-v4/Cargo.toml @@ -28,6 +28,7 @@ bincode = "1.3.3" borsh = { version = "0.9.3", features = ["const-generics"] } bytemuck = { version = "^1.7.2", features = ["min_const_generics"] } checked_math = { path = "../../lib/checked_math" } +default-env = "0.1.1" derivative = "2.2.0" fixed = { version = "=1.11.0", features = ["serde", "borsh"] } # todo: higher versions don't work fixed-macro = "^1.1.1" @@ -38,6 +39,7 @@ serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation solana-address-lookup-table-program = "~1.14.9" solana-program = "~1.14.9" solana-sdk = { version = "~1.14.9", default-features = false, optional = true } +solana-security-txt = "1.1.0" static_assertions = "1.1" switchboard-program = ">=0.2.0" switchboard-v2 = "0.1.17" diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 1a359c5cd..ae22b0e86 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -760,3 +760,17 @@ impl anchor_lang::Id for Mango { ID } } + +#[cfg(not(feature = "no-entrypoint"))] +use {default_env::default_env, solana_security_txt::security_txt}; +#[cfg(not(feature = "no-entrypoint"))] +security_txt! { + name: "Mango v4", + project_url: "https://mango.markets", + contacts: "email:hello@blockworks.foundation,link:https://docs.mango.markets/mango-markets/bug-bounty,discord:https://discord.gg/mangomarkets", + policy: "https://github.com/blockworks-foundation/mango-v4/blob/main/SECURITY.md", + preferred_languages: "en", + source_code: "https://github.com/blockworks-foundation/mango-v4", + source_revision: default_env!("GITHUB_SHA", "Unknown source revision"), + source_release: default_env!("GITHUB_REF_NAME", "Unknown source release") +} From e4f238f489243d7ed33ba124f85d92821eea74ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezi=C5=84ski?= Date: Thu, 5 Jan 2023 08:30:15 +0100 Subject: [PATCH 4/5] combine open orders create with place order (#362) * wip * place order fix * fixes * comment * cleanup Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 Co-authored-by: microwavedcola1 --- ts/client/src/accounts/serum3.ts | 16 +++ ts/client/src/client.ts | 161 ++++++++++++++++++++++++------- ts/client/src/types.ts | 8 ++ 3 files changed, 152 insertions(+), 33 deletions(-) diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index 554a79c83..4fe5f8cf6 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -56,6 +56,22 @@ export class Serum3Market { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; } + public async findOoPda( + programId: PublicKey, + mangoAccount: PublicKey, + ): Promise { + const [openOrderPublicKey] = await PublicKey.findProgramAddress( + [ + Buffer.from('Serum3OO'), + mangoAccount.toBuffer(), + this.publicKey.toBuffer(), + ], + programId, + ); + + return openOrderPublicKey; + } + public getFeeRates(taker = true): number { // See https://github.com/openbook-dex/program/blob/master/dex/src/fees.rs#L81 const ratesBps = diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 9cd81049d..f64f95dcb 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -48,7 +48,12 @@ import { OPENBOOK_PROGRAM_ID } from './constants'; import { Id } from './ids'; import { IDL, MangoV4 } from './mango_v4'; import { I80F48 } from './numbers/I80F48'; -import { FlashLoanType, InterestRateParams, OracleConfigParams } from './types'; +import { + AdditionalHealthAccounts, + FlashLoanType, + InterestRateParams, + OracleConfigParams, +} from './types'; import { I64_MAX_BN, createAssociatedTokenAccountIdempotentInstruction, @@ -1102,11 +1107,36 @@ export class MangoClient { .rpc(); } - public async serum3CloseOpenOrders( + public async serum3CreateOpenOrdersIx( group: Group, mangoAccount: MangoAccount, externalMarketPk: PublicKey, - ): Promise { + ): Promise { + const serum3Market: Serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; + + const ix = await this.program.methods + .serum3CreateOpenOrders() + .accounts({ + group: group.publicKey, + account: mangoAccount.publicKey, + serumMarket: serum3Market.publicKey, + serumProgram: serum3Market.serumProgram, + serumMarketExternal: serum3Market.serumMarketExternal, + owner: (this.program.provider as AnchorProvider).wallet.publicKey, + payer: (this.program.provider as AnchorProvider).wallet.publicKey, + }) + .instruction(); + + return ix; + } + + public async serum3CloseOpenOrdersIx( + group: Group, + mangoAccount: MangoAccount, + externalMarketPk: PublicKey, + ): Promise { const serum3Market = group.serum3MarketsMapByExternal.get( externalMarketPk.toBase58(), )!; @@ -1127,7 +1157,28 @@ export class MangoClient { solDestination: (this.program.provider as AnchorProvider).wallet .publicKey, }) - .rpc(); + .instruction(); + } + + public async serum3CloseOpenOrders( + group: Group, + mangoAccount: MangoAccount, + externalMarketPk: PublicKey, + ): Promise { + const ix = await this.serum3CloseOpenOrdersIx( + group, + mangoAccount, + externalMarketPk, + ); + + return await sendTransaction( + this.program.provider as AnchorProvider, + [ix], + group.addressLookupTablesList, + { + postSendTxCallback: this.postSendTxCallback, + }, + ); } public async serum3PlaceOrderIx( @@ -1141,27 +1192,44 @@ export class MangoClient { orderType: Serum3OrderType, clientOrderId: number, limit: number, - ): Promise { + ): Promise { + const ixs: TransactionInstruction[] = []; const serum3Market = group.serum3MarketsMapByExternal.get( externalMarketPk.toBase58(), )!; + + let ooPk; + let additionalAccounts: AdditionalHealthAccounts | undefined = undefined; if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) { - await this.serum3CreateOpenOrders( + const ix = await this.serum3CreateOpenOrdersIx( group, mangoAccount, serum3Market.serumMarketExternal, ); - await mangoAccount.reload(this); - } - const serum3MarketExternal = group.serum3ExternalMarketsMap.get( - externalMarketPk.toBase58(), - )!; - const serum3MarketExternalVaultSigner = - await generateSerum3MarketExternalVaultSignerAddress( - this.cluster, - serum3Market, - serum3MarketExternal, + + ooPk = await serum3Market.findOoPda( + this.program.programId, + mangoAccount.publicKey, ); + const tokenIndex = + serum3Market[ + side == Serum3Side.bid ? 'baseTokenIndex' : 'quoteTokenIndex' + ]; + const baseBank = group.getFirstBankByTokenIndex(tokenIndex); + + // only push bank/oracle if no deposit has been previously made for same token + const hasBaseBank = + mangoAccount.tokens[tokenIndex].tokenIndex !== + TokenPosition.TokenIndexUnset; + + additionalAccounts = { + banks: !hasBaseBank ? [baseBank.publicKey] : [], + oracles: !hasBaseBank ? [baseBank.oracle] : [], + openOrders: [ooPk], + perps: [], + }; + ixs.push(ix); + } const healthRemainingAccounts: PublicKey[] = this.buildHealthRemainingAccounts( @@ -1170,6 +1238,17 @@ export class MangoClient { [mangoAccount], [], [], + additionalAccounts, + ); + + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( + externalMarketPk.toBase58(), + )!; + const serum3MarketExternalVaultSigner = + await generateSerum3MarketExternalVaultSignerAddress( + this.cluster, + serum3Market, + serum3MarketExternal, ); const limitPrice = serum3MarketExternal.priceNumberToLots(price); @@ -1191,7 +1270,6 @@ export class MangoClient { })(); const payerBank = group.getFirstBankByTokenIndex(payerTokenIndex); - const ix = await this.program.methods .serum3PlaceOrder( side, @@ -1207,8 +1285,9 @@ export class MangoClient { group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, - openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) - ?.openOrders, + openOrders: + ooPk || + mangoAccount.getSerum3Account(serum3Market.marketIndex)?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: OPENBOOK_PROGRAM_ID[this.cluster], serumMarketExternal: serum3Market.serumMarketExternal, @@ -1231,7 +1310,9 @@ export class MangoClient { ) .instruction(); - return ix; + ixs.push(ix); + + return ixs; } public async serum3PlaceOrder( @@ -1246,7 +1327,7 @@ export class MangoClient { clientOrderId: number, limit: number, ): Promise { - const ix = await this.serum3PlaceOrderIx( + const placeOrderIxes = await this.serum3PlaceOrderIx( group, mangoAccount, externalMarketPk, @@ -1258,15 +1339,14 @@ export class MangoClient { clientOrderId, limit, ); - - const ix2 = await this.serum3SettleFundsIx( + const settleIx = await this.serum3SettleFundsIx( group, mangoAccount, externalMarketPk, ); return await this.sendAndConfirmTransaction( - [ix, ix2], + [...placeOrderIxes, settleIx], group.addressLookupTablesList, ); } @@ -1319,12 +1399,16 @@ export class MangoClient { const serum3MarketExternal = group.serum3ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; - const serum3MarketExternalVaultSigner = - await generateSerum3MarketExternalVaultSignerAddress( - this.cluster, - serum3Market, - serum3MarketExternal, - ); + + const [serum3MarketExternalVaultSigner, openOrderPublicKey] = + await Promise.all([ + generateSerum3MarketExternalVaultSignerAddress( + this.cluster, + serum3Market, + serum3MarketExternal, + ), + serum3Market.findOoPda(this.program.programId, mangoAccount.publicKey), + ]); const ix = await this.program.methods .serum3SettleFunds() @@ -1332,8 +1416,7 @@ export class MangoClient { group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, - openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex) - ?.openOrders, + openOrders: openOrderPublicKey, serumMarket: serum3Market.publicKey, serumProgram: OPENBOOK_PROGRAM_ID[this.cluster], serumMarketExternal: serum3Market.serumMarketExternal, @@ -2490,6 +2573,7 @@ export class MangoClient { mangoAccounts: MangoAccount[], banks: Bank[], perpMarkets: PerpMarket[], + additionalAccounts?: AdditionalHealthAccounts, ): PublicKey[] { if (retriever === AccountRetriever.Fixed) { return this.buildFixedAccountRetrieverHealthAccounts( @@ -2497,6 +2581,7 @@ export class MangoClient { mangoAccounts[0], banks, perpMarkets, + additionalAccounts, ); } else { return this.buildScanningAccountRetrieverHealthAccounts( @@ -2515,6 +2600,12 @@ export class MangoClient { // but user would potentially open new positions. banks: Bank[], perpMarkets: PerpMarket[], + additionalAccounts: AdditionalHealthAccounts = { + oracles: [], + banks: [], + perps: [], + openOrders: [], + }, ): PublicKey[] { const healthRemainingAccounts: PublicKey[] = []; @@ -2541,9 +2632,11 @@ export class MangoClient { healthRemainingAccounts.push( ...mintInfos.map((mintInfo) => mintInfo.firstBank()), + ...additionalAccounts.banks, ); healthRemainingAccounts.push( ...mintInfos.map((mintInfo) => mintInfo.oracle), + ...additionalAccounts.oracles, ); const allPerpIndices = mangoAccount.perps.map((perp) => perp.marketIndex); @@ -2567,6 +2660,7 @@ export class MangoClient { .map((index) => group.findPerpMarket(index)!); healthRemainingAccounts.push( ...allPerpMarkets.map((perp) => perp.publicKey), + ...additionalAccounts.perps, ); healthRemainingAccounts.push(...allPerpMarkets.map((perp) => perp.oracle)); @@ -2574,6 +2668,7 @@ export class MangoClient { ...mangoAccount.serum3 .filter((serum3Account) => serum3Account.marketIndex !== 65535) .map((serum3Account) => serum3Account.openOrders), + ...additionalAccounts.openOrders, ); // debugHealthAccounts(group, mangoAccount, healthRemainingAccounts); @@ -2720,7 +2815,7 @@ export class MangoClient { limit, ), ]); - transactionInstructions.push(cancelOrderIx, settleIx, placeOrderIx); + transactionInstructions.push(cancelOrderIx, settleIx, ...placeOrderIx); return await this.sendAndConfirmTransaction( transactionInstructions, diff --git a/ts/client/src/types.ts b/ts/client/src/types.ts index a93cc3c66..29371b61b 100644 --- a/ts/client/src/types.ts +++ b/ts/client/src/types.ts @@ -1,4 +1,5 @@ import { BN } from '@project-serum/anchor'; +import { PublicKey } from '@solana/web3.js'; export type Modify = Omit & R; @@ -25,3 +26,10 @@ export class OracleConfigParams { confFilter: number; maxStalenessSlots: number | null; } + +export type AdditionalHealthAccounts = { + banks: PublicKey[]; + oracles: PublicKey[]; + perps: PublicKey[]; + openOrders: PublicKey[]; +}; From 292cc9a450afa0c0bcf379a2e6a49c1900b2a731 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Thu, 5 Jan 2023 20:08:45 +0100 Subject: [PATCH 5/5] bonk (#363) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- .../src/instructions/token_register_trustless.rs | 2 +- ts/client/src/scripts/mb-admin.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index 85707e364..49cca4833 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -84,7 +84,7 @@ pub fn token_register_trustless( oracle: ctx.accounts.oracle.key(), oracle_config: OracleConfig { conf_filter: I80F48::from_num(0.10), - max_staleness_slots: -1, + max_staleness_slots: 600, reserved: [0; 72], }, stable_price_model: StablePriceModel::default(), diff --git a/ts/client/src/scripts/mb-admin.ts b/ts/client/src/scripts/mb-admin.ts index 105f5b15b..10400db5f 100644 --- a/ts/client/src/scripts/mb-admin.ts +++ b/ts/client/src/scripts/mb-admin.ts @@ -36,6 +36,7 @@ const MAINNET_MINTS = new Map([ ['SOL', 'So11111111111111111111111111111111111111112'], // 4 Wrapped SOL ['MSOL', 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'], // 5 ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], // 6 + ['BONK', 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263'], // 7 ]); const MAINNET_ORACLES = new Map([ // USDC - stub oracle @@ -46,6 +47,7 @@ const MAINNET_ORACLES = new Map([ ['MSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'], ['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'], ['BTC', 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'], + ['BONK', '4SZ1qb4MtSUrZcoeaeQ3BDzVCyqxw3VwSFpPiMTmn4GE'], ]); // External markets are matched with those in https://github.com/openbook-dex/openbook-ts/blob/master/packages/serum/src/markets.json @@ -322,6 +324,17 @@ async function registerTokens() { 'MNGO', ); + console.log(`Registering BONK...`); + const bonkMainnetMint = new PublicKey(MAINNET_MINTS.get('BONK')!); + const bonkMainnetOracle = new PublicKey(MAINNET_ORACLES.get('BONK')!); + await client.tokenRegisterTrustless( + group, + bonkMainnetMint, + bonkMainnetOracle, + 7, + 'BONK', + ); + // log tokens/banks await group.reloadAll(client); for (const bank of await Array.from(group.banksMapByMint.values())