#![allow(dead_code)] use super::*; use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::serum3_cpi::{load_open_orders_bytes, OpenOrdersSlim}; use std::sync::Arc; struct SerumOrderPlacer { solana: Arc, serum: Arc, account: Pubkey, owner: TestKeypair, serum_market: Pubkey, open_orders: Pubkey, next_client_order_id: u64, } impl SerumOrderPlacer { fn inc_client_order_id(&mut self) -> u64 { let id = self.next_client_order_id; self.next_client_order_id += 1; id } async fn find_order_id_for_client_order_id(&self, client_order_id: u64) -> Option<(u128, u64)> { let open_orders = self.serum.load_open_orders(self.open_orders).await; for i in 0..128 { if open_orders.free_slot_bits & (1u128 << i) != 0 { continue; } if open_orders.client_order_ids[i] == client_order_id { return Some((open_orders.orders[i], client_order_id)); } } None } async fn try_bid( &mut self, limit_price: f64, max_base: u64, taker: bool, ) -> Result { let client_order_id = self.inc_client_order_id(); let fees = if taker { 0.0004 } else { 0.0 }; send_tx( &self.solana, Serum3PlaceOrderInstruction { side: Serum3Side::Bid, limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100) max_base_qty: max_base / 100, // in base lot (100) // 4 bps taker fees added in max_native_quote_qty_including_fees: (limit_price * (max_base as f64) * (1.0 + fees)) .ceil() as u64, self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction, order_type: Serum3OrderType::Limit, client_order_id, limit: 10, account: self.account, owner: self.owner, serum_market: self.serum_market, }, ) .await } async fn bid_maker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> { self.try_bid(limit_price, max_base, false).await.unwrap(); self.find_order_id_for_client_order_id(self.next_client_order_id - 1) .await } async fn bid_taker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> { self.try_bid(limit_price, max_base, true).await.unwrap(); self.find_order_id_for_client_order_id(self.next_client_order_id - 1) .await } async fn try_ask( &mut self, limit_price: f64, max_base: u64, ) -> Result { let client_order_id = self.inc_client_order_id(); send_tx( &self.solana, Serum3PlaceOrderInstruction { side: Serum3Side::Ask, limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100) max_base_qty: max_base / 100, // in base lot (100) max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64, self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction, order_type: Serum3OrderType::Limit, client_order_id, limit: 10, account: self.account, owner: self.owner, serum_market: self.serum_market, }, ) .await } async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> { self.try_ask(limit_price, max_base).await.unwrap(); self.find_order_id_for_client_order_id(self.next_client_order_id - 1) .await } async fn cancel(&self, order_id: u128) { let side = { let open_orders = self.serum.load_open_orders(self.open_orders).await; let orders = open_orders.orders; let idx = orders.iter().position(|&v| v == order_id).unwrap(); if open_orders.is_bid_bits & (1u128 << idx) == 0 { Serum3Side::Ask } else { Serum3Side::Bid } }; send_tx( &self.solana, Serum3CancelOrderInstruction { side, order_id, account: self.account, owner: self.owner, serum_market: self.serum_market, }, ) .await .unwrap(); } async fn cancel_all(&self) { let open_orders = self.serum.load_open_orders(self.open_orders).await; let orders = open_orders.orders; for (idx, order_id) in orders.iter().enumerate() { if *order_id == 0 { continue; } let side = if open_orders.is_bid_bits & (1u128 << idx) == 0 { Serum3Side::Ask } else { Serum3Side::Bid }; send_tx( &self.solana, Serum3CancelOrderInstruction { side, order_id: *order_id, account: self.account, owner: self.owner, serum_market: self.serum_market, }, ) .await .unwrap(); } } async fn settle(&self) { self.settle_v2(true).await } async fn settle_v2(&self, fees_to_dao: bool) { send_tx( &self.solana, Serum3SettleFundsV2Instruction { account: self.account, owner: self.owner, serum_market: self.serum_market, fees_to_dao, }, ) .await .unwrap(); } async fn mango_serum_orders(&self) -> Serum3Orders { let account_data = get_mango_account(&self.solana, self.account).await; let orders = account_data .all_serum3_orders() .find(|s| s.open_orders == self.open_orders) .unwrap(); orders.clone() } async fn _open_orders(&self) -> OpenOrdersSlim { let data = self .solana .get_account_data(self.open_orders) .await .unwrap(); OpenOrdersSlim::from_oo(load_open_orders_bytes(&data).unwrap()) } } #[tokio::test] async fn test_serum_basics() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); let admin = TestKeypair::new(); let owner = context.users[0].key; let payer = context.users[1].key; let mints = &context.mints[0..2]; // // SETUP: Create a group and an account // let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints: mints.to_vec(), ..GroupWithTokensConfig::default() } .create(solana) .await; let base_token = &tokens[0]; let quote_token = &tokens[1]; // // SETUP: Create serum market // let serum_market_cookie = context .serum .list_spot_market(&base_token.mint, "e_token.mint) .await; // // TEST: Register a serum market // let serum_market = send_tx( solana, Serum3RegisterMarketInstruction { group, admin, serum_program: context.serum.program_id, serum_market_external: serum_market_cookie.market, market_index: 0, base_bank: base_token.bank, quote_bank: quote_token.bank, payer, }, ) .await .unwrap() .serum_market; // // SETUP: Create account // let deposit_amount = 1000; let account = create_funded_account( &solana, group, owner, 0, &context.users[1], mints, deposit_amount, 0, ) .await; // // TEST: Create an open orders account // let open_orders = send_tx( solana, Serum3CreateOpenOrdersInstruction { account, serum_market, owner, payer, }, ) .await .unwrap() .open_orders; let account_data = get_mango_account(solana, account).await; assert_eq!( account_data .active_serum3_orders() .map(|v| (v.open_orders, v.market_index)) .collect::>(), [(open_orders, 0)] ); let mut order_placer = SerumOrderPlacer { solana: solana.clone(), serum: context.serum.clone(), account, owner: owner.clone(), serum_market, open_orders, next_client_order_id: 0, }; // // TEST: Place an order // let (order_id, _) = order_placer.bid_maker(1.0, 100).await.unwrap(); check_prev_instruction_post_health(&solana, account).await; let native0 = account_position(solana, account, base_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await; assert_eq!(native0, 1000); assert_eq!(native1, 900); let account_data = get_mango_account(solana, account).await; assert_eq!( account_data .token_position_by_raw_index(0) .unwrap() .in_use_count, 1 ); assert_eq!( account_data .token_position_by_raw_index(1) .unwrap() .in_use_count, 1 ); assert_eq!( account_data .token_position_by_raw_index(2) .unwrap() .in_use_count, 0 ); let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); assert_eq!(serum_orders.base_borrows_without_fee, 0); assert_eq!(serum_orders.quote_borrows_without_fee, 0); assert_eq!(serum_orders.base_deposits_reserved, 0); assert_eq!(serum_orders.quote_deposits_reserved, 100); let base_bank = solana.get_account::(base_token.bank).await; assert_eq!(base_bank.deposits_in_serum, 0); let quote_bank = solana.get_account::(quote_token.bank).await; assert_eq!(quote_bank.deposits_in_serum, 100); assert!(order_id != 0); // // TEST: Cancel the order // order_placer.cancel(order_id).await; // // TEST: Settle, moving the freed up funds back // order_placer.settle().await; let native0 = account_position(solana, account, base_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await; assert_eq!(native0, 1000); assert_eq!(native1, 1000); let account_data = get_mango_account(solana, account).await; let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); assert_eq!(serum_orders.base_borrows_without_fee, 0); assert_eq!(serum_orders.quote_borrows_without_fee, 0); assert_eq!(serum_orders.base_deposits_reserved, 0); assert_eq!(serum_orders.quote_deposits_reserved, 0); let base_bank = solana.get_account::(base_token.bank).await; assert_eq!(base_bank.deposits_in_serum, 0); let quote_bank = solana.get_account::(quote_token.bank).await; assert_eq!(quote_bank.deposits_in_serum, 0); // Process events such that the OutEvent deactivates the closed order on open_orders context .serum .consume_spot_events(&serum_market_cookie, &[open_orders]) .await; // close oo account send_tx( solana, Serum3CloseOpenOrdersInstruction { account, serum_market, owner, sol_destination: payer.pubkey(), }, ) .await .unwrap(); let account_data = get_mango_account(solana, account).await; assert_eq!( account_data .token_position_by_raw_index(0) .unwrap() .in_use_count, 0 ); assert_eq!( account_data .token_position_by_raw_index(1) .unwrap() .in_use_count, 0 ); // deregister serum3 market send_tx( solana, Serum3DeregisterMarketInstruction { group, admin, serum_market_external: serum_market_cookie.market, sol_destination: payer.pubkey(), }, ) .await .unwrap(); Ok(()) } #[tokio::test] async fn test_serum_loan_origination_fees() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(100_000); // Serum3PlaceOrder needs 95.1k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 180000; let CommonSetup { serum_market_cookie, quote_token, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; let quote_bank = quote_token.bank; let base_bank = base_token.bank; let account = order_placer.account; let account2 = order_placer2.account; // // TEST: Placing and canceling an order does not take loan origination fees even if borrows are needed // { let (bid_order_id, _) = order_placer.bid_maker(1.0, 200000).await.unwrap(); let (ask_order_id, _) = order_placer.ask(2.0, 200000).await.unwrap(); let o = order_placer.mango_serum_orders().await; assert_eq!(o.base_borrows_without_fee, 19999); // rounded assert_eq!(o.quote_borrows_without_fee, 19999); order_placer.cancel(bid_order_id).await; order_placer.cancel(ask_order_id).await; let o = order_placer.mango_serum_orders().await; assert_eq!(o.base_borrows_without_fee, 19999); // unchanged assert_eq!(o.quote_borrows_without_fee, 19999); // placing new, slightly larger orders increases the borrow_without_fee amount only by a small amount let (bid_order_id, _) = order_placer.bid_maker(1.0, 210000).await.unwrap(); let (ask_order_id, _) = order_placer.ask(2.0, 300000).await.unwrap(); let o = order_placer.mango_serum_orders().await; assert_eq!(o.base_borrows_without_fee, 119998); // rounded assert_eq!(o.quote_borrows_without_fee, 29998); order_placer.cancel(bid_order_id).await; order_placer.cancel(ask_order_id).await; // returns all the funds order_placer.settle().await; let o = order_placer.mango_serum_orders().await; assert_eq!(o.base_borrows_without_fee, 0); assert_eq!(o.quote_borrows_without_fee, 0); assert_eq!( account_position(solana, account, quote_bank).await, deposit_amount as i64 ); assert_eq!( account_position(solana, account, base_bank).await, deposit_amount as i64 ); // consume all the out events from the cancels context .serum .consume_spot_events(&serum_market_cookie, &[order_placer.open_orders]) .await; } let without_serum_taker_fee = |amount: i64| (amount as f64 * (1.0 - 0.0004)).trunc() as i64; let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0002).floor() as i64; let serum_fee = |amount: i64| (amount as f64 * 0.0002).trunc() as i64; let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64; // // TEST: Order execution and settling charges borrow fee // { let deposit_amount = deposit_amount as i64; let bid_amount = 200000; let ask_amount = 210000; let fill_amount = 200000; let quote_fees1 = solana .get_account::(quote_bank) .await .collected_fees_native; // account2 has an order on the book order_placer2 .bid_maker(1.0, bid_amount as u64) .await .unwrap(); // account takes order_placer.ask(1.0, ask_amount as u64).await.unwrap(); order_placer.settle().await; let o = order_placer.mango_serum_orders().await; // parts of the order ended up on the book an may cause loan origination fees later assert_eq!( o.base_borrows_without_fee, (ask_amount - fill_amount) as u64 ); assert_eq!(o.quote_borrows_without_fee, 0); assert_eq!( account_position(solana, account, quote_bank).await, deposit_amount + without_serum_taker_fee(fill_amount) ); assert_eq!( account_position(solana, account, base_bank).await, deposit_amount - ask_amount - loan_origination_fee(fill_amount - deposit_amount) ); // Serum referrer rebates only accrue once the events are executed let quote_fees2 = solana .get_account::(quote_bank) .await .collected_fees_native; assert!(assert_equal(quote_fees2 - quote_fees1, 0.0, 0.1)); // check account2 balances too context .serum .consume_spot_events( &serum_market_cookie, &[order_placer.open_orders, order_placer2.open_orders], ) .await; order_placer2.settle().await; let o = order_placer2.mango_serum_orders().await; assert_eq!(o.base_borrows_without_fee, 0); assert_eq!(o.quote_borrows_without_fee, 0); assert_eq!( account_position(solana, account2, base_bank).await, deposit_amount + fill_amount ); assert_eq!( account_position(solana, account2, quote_bank).await, deposit_amount - fill_amount - loan_origination_fee(fill_amount - deposit_amount) + (serum_maker_rebate(fill_amount) - 1) // unclear where the -1 comes from? ); // Serum referrer rebates accrue on the taker side let quote_fees3 = solana .get_account::(quote_bank) .await .collected_fees_native; assert!(assert_equal( quote_fees3 - quote_fees1, loan_origination_fee(fill_amount - deposit_amount) as f64, 0.1 )); order_placer.settle().await; // Now rebates got collected as Mango fees, but user balances are unchanged let quote_fees4 = solana .get_account::(quote_bank) .await .collected_fees_native; assert!(assert_equal( quote_fees4 - quote_fees3, serum_fee(fill_amount) as f64, 0.1 )); let account_data = solana.get_account::(account).await; assert_eq!( account_data.buyback_fees_accrued_current, serum_maker_rebate(fill_amount) as u64 ); assert_eq!( account_position(solana, account, quote_bank).await, deposit_amount + without_serum_taker_fee(fill_amount) ); } Ok(()) } #[tokio::test] async fn test_serum_settle_v1() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 160000; let CommonSetup { serum_market_cookie, quote_token, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; let quote_bank = quote_token.bank; let base_bank = base_token.bank; let account = order_placer.account; let account2 = order_placer2.account; let serum_taker_fee = |amount: i64| (amount as f64 * 0.0004).trunc() as i64; let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0002).floor() as i64; let serum_referrer_fee = |amount: i64| (amount as f64 * 0.0002).trunc() as i64; let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64; // // TEST: Use v1 serum3_settle_funds // let deposit_amount = deposit_amount as i64; let amount = 200000; let quote_fees_start = solana .get_account::(quote_bank) .await .collected_fees_native; let quote_start = account_position(solana, account, quote_bank).await; let quote2_start = account_position(solana, account2, quote_bank).await; let base_start = account_position(solana, account, base_bank).await; let base2_start = account_position(solana, account2, base_bank).await; // account2 has an order on the book, account takes order_placer2.bid_maker(1.0, amount as u64).await.unwrap(); order_placer.ask(1.0, amount as u64).await.unwrap(); context .serum .consume_spot_events( &serum_market_cookie, &[order_placer.open_orders, order_placer2.open_orders], ) .await; order_placer.settle().await; order_placer2.settle().await; let quote_end = account_position(solana, account, quote_bank).await; let quote2_end = account_position(solana, account2, quote_bank).await; let base_end = account_position(solana, account, base_bank).await; let base2_end = account_position(solana, account2, base_bank).await; let lof = loan_origination_fee(amount - deposit_amount); assert_eq!(base_start - amount - lof, base_end); assert_eq!(base2_start + amount, base2_end); assert_eq!(quote_start + amount - serum_taker_fee(amount), quote_end); assert_eq!( quote2_start - amount + serum_maker_rebate(amount) - lof - 1, quote2_end ); let quote_fees_end = solana .get_account::(quote_bank) .await .collected_fees_native; assert!(assert_equal( quote_fees_end - quote_fees_start, (lof + serum_referrer_fee(amount)) as f64, 0.1 )); Ok(()) } #[tokio::test] async fn test_serum_settle_v2_to_dao() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 160000; let CommonSetup { group_with_tokens, serum_market_cookie, quote_token, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; let quote_bank = quote_token.bank; let base_bank = base_token.bank; let account = order_placer.account; let account2 = order_placer2.account; // Change the quote price to verify that the current value of the serum quote token // is added to the buyback fees amount set_bank_stub_oracle_price( solana, group_with_tokens.group, "e_token, group_with_tokens.admin, 2.0, ) .await; let serum_taker_fee = |amount: i64| (amount as f64 * 0.0004).trunc() as i64; let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0002).floor() as i64; let serum_referrer_fee = |amount: i64| (amount as f64 * 0.0002).trunc() as i64; let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64; // // TEST: Use v2 serum3_settle_funds // let deposit_amount = deposit_amount as i64; let amount = 200000; let quote_fees_start = solana .get_account::(quote_bank) .await .collected_fees_native; let quote_start = account_position(solana, account, quote_bank).await; let quote2_start = account_position(solana, account2, quote_bank).await; let base_start = account_position(solana, account, base_bank).await; let base2_start = account_position(solana, account2, base_bank).await; // account2 has an order on the book, account takes order_placer2.bid_maker(1.0, amount as u64).await.unwrap(); order_placer.ask(1.0, amount as u64).await.unwrap(); context .serum .consume_spot_events( &serum_market_cookie, &[order_placer.open_orders, order_placer2.open_orders], ) .await; order_placer.settle_v2(true).await; order_placer2.settle_v2(true).await; let quote_end = account_position(solana, account, quote_bank).await; let quote2_end = account_position(solana, account2, quote_bank).await; let base_end = account_position(solana, account, base_bank).await; let base2_end = account_position(solana, account2, base_bank).await; let lof = loan_origination_fee(amount - deposit_amount); assert_eq!(base_start - amount - lof, base_end); assert_eq!(base2_start + amount, base2_end); assert_eq!(quote_start + amount - serum_taker_fee(amount), quote_end); assert_eq!( quote2_start - amount + serum_maker_rebate(amount) - lof - 1, quote2_end ); let quote_fees_end = solana .get_account::(quote_bank) .await .collected_fees_native; assert!(assert_equal( quote_fees_end - quote_fees_start, (lof + serum_referrer_fee(amount)) as f64, 0.1 )); let account_data = solana.get_account::(account).await; assert_eq!( account_data.buyback_fees_accrued_current, (serum_maker_rebate(amount) * 2) as u64 // *2 because that's the quote price and this number is in $ ); let account2_data = solana.get_account::(account2).await; assert_eq!(account2_data.buyback_fees_accrued_current, 0); Ok(()) } #[tokio::test] async fn test_serum_settle_v2_to_account() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 160000; let CommonSetup { serum_market_cookie, quote_token, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; let quote_bank = quote_token.bank; let base_bank = base_token.bank; let account = order_placer.account; let account2 = order_placer2.account; let serum_taker_fee = |amount: i64| (amount as f64 * 0.0004).trunc() as i64; let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0002).floor() as i64; let serum_referrer_fee = |amount: i64| (amount as f64 * 0.0002).trunc() as i64; let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64; // // TEST: Use v1 serum3_settle_funds // let deposit_amount = deposit_amount as i64; let amount = 200000; let quote_fees_start = solana .get_account::(quote_bank) .await .collected_fees_native; let quote_start = account_position(solana, account, quote_bank).await; let quote2_start = account_position(solana, account2, quote_bank).await; let base_start = account_position(solana, account, base_bank).await; let base2_start = account_position(solana, account2, base_bank).await; // account2 has an order on the book, account takes order_placer2.bid_maker(1.0, amount as u64).await.unwrap(); order_placer.ask(1.0, amount as u64).await.unwrap(); context .serum .consume_spot_events( &serum_market_cookie, &[order_placer.open_orders, order_placer2.open_orders], ) .await; order_placer.settle_v2(false).await; order_placer2.settle_v2(false).await; let quote_end = account_position(solana, account, quote_bank).await; let quote2_end = account_position(solana, account2, quote_bank).await; let base_end = account_position(solana, account, base_bank).await; let base2_end = account_position(solana, account2, base_bank).await; let lof = loan_origination_fee(amount - deposit_amount); assert_eq!(base_start - amount - lof, base_end); assert_eq!(base2_start + amount, base2_end); assert_eq!( quote_start + amount - serum_taker_fee(amount) + serum_referrer_fee(amount), quote_end ); assert_eq!( quote2_start - amount + serum_maker_rebate(amount) - lof - 1, quote2_end ); let quote_fees_end = solana .get_account::(quote_bank) .await .collected_fees_native; assert!(assert_equal( quote_fees_end - quote_fees_start, lof as f64, 0.1 )); let account_data = solana.get_account::(account).await; assert_eq!(account_data.buyback_fees_accrued_current, 0); let account2_data = solana.get_account::(account2).await; assert_eq!(account2_data.buyback_fees_accrued_current, 0); Ok(()) } #[tokio::test] async fn test_serum_reduce_only_borrows() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 1000; let CommonSetup { group_with_tokens, base_token, mut order_placer, .. } = common_setup(&context, deposit_amount).await; send_tx( solana, TokenMakeReduceOnly { group: group_with_tokens.group, admin: group_with_tokens.admin, mint: base_token.mint.pubkey, reduce_only: 2, force_close: false, }, ) .await .unwrap(); // // TEST: Cannot borrow tokens when bank is reduce only // let err = order_placer.try_ask(1.0, 1100).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); order_placer.try_ask(0.5, 500).await.unwrap(); let err = order_placer.try_ask(1.0, 600).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); order_placer.try_ask(2.0, 500).await.unwrap(); let err = order_placer.try_ask(1.0, 100).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); Ok(()) } #[tokio::test] async fn test_serum_reduce_only_deposits1() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 1000; let CommonSetup { group_with_tokens, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; send_tx( solana, TokenMakeReduceOnly { group: group_with_tokens.group, admin: group_with_tokens.admin, mint: base_token.mint.pubkey, reduce_only: 1, force_close: false, }, ) .await .unwrap(); // // TEST: Cannot buy tokens when deposits are already >0 // // fails to place on the book let err = order_placer.try_bid(1.0, 1000, false).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); // also fails as a taker order order_placer2.ask(1.0, 500).await.unwrap(); let err = order_placer.try_bid(1.0, 100, true).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); Ok(()) } #[tokio::test] async fn test_serum_reduce_only_deposits2() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 1000; let CommonSetup { group_with_tokens, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; // Give account some base token borrows (-500) send_tx( solana, TokenWithdrawInstruction { amount: 1500, allow_borrow: true, account: order_placer.account, owner: order_placer.owner, token_account: context.users[0].token_accounts[1], bank_index: 0, }, ) .await .unwrap(); // // TEST: Cannot buy tokens when deposits are already >0 // send_tx( solana, TokenMakeReduceOnly { group: group_with_tokens.group, admin: group_with_tokens.admin, mint: base_token.mint.pubkey, reduce_only: 1, force_close: false, }, ) .await .unwrap(); // cannot place a large order on the book that would deposit too much let err = order_placer.try_bid(1.0, 600, false).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); // a small order is fine order_placer.try_bid(1.0, 100, false).await.unwrap(); // taking some is fine too order_placer2.ask(1.0, 800).await.unwrap(); order_placer.try_bid(1.0, 100, true).await.unwrap(); // the limit for orders is reduced now, 100 received, 100 on the book let err = order_placer.try_bid(1.0, 400, true).await; assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into()); Ok(()) } #[tokio::test] async fn test_serum_place_reducing_when_liquidatable() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots let context = test_builder.start_default().await; let solana = &context.solana.clone(); // // SETUP: Create a group, accounts, market etc // let deposit_amount = 1000; let CommonSetup { group_with_tokens, base_token, mut order_placer, .. } = common_setup(&context, deposit_amount).await; // Give account some base token borrows (-500) send_tx( solana, TokenWithdrawInstruction { amount: 1500, allow_borrow: true, account: order_placer.account, owner: order_placer.owner, token_account: context.users[0].token_accounts[1], bank_index: 0, }, ) .await .unwrap(); // Change the base price to make the account liquidatable set_bank_stub_oracle_price( solana, group_with_tokens.group, &base_token, group_with_tokens.admin, 10.0, ) .await; assert!(account_init_health(solana, order_placer.account).await < 0.0); // can place an order that would close some of the borrows order_placer.try_bid(10.0, 200, false).await.unwrap(); // if too much base is bought, health would decrease: forbidden let err = order_placer.try_bid(10.0, 800, false).await; assert_mango_error( &err, MangoError::HealthMustBePositiveOrIncrease.into(), "".into(), ); Ok(()) } #[tokio::test] async fn test_serum_track_bid_ask() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots let context = test_builder.start_default().await; // // SETUP: Create a group, accounts, market etc // let deposit_amount = 10000; let CommonSetup { serum_market_cookie, mut order_placer, .. } = common_setup(&context, deposit_amount).await; // // TEST: highest bid/lowest ask updating // assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 0.0 ); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 0.0 ); order_placer.bid_maker(10.0, 100).await.unwrap(); assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 1.0 / 10.0 ); order_placer.bid_maker(9.0, 100).await.unwrap(); assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 1.0 / 10.0 ); order_placer.bid_maker(11.0, 100).await.unwrap(); assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 1.0 / 11.0 ); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 0.0 ); order_placer.ask(20.0, 100).await.unwrap(); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 20.0 ); order_placer.ask(19.0, 100).await.unwrap(); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 19.0 ); order_placer.ask(21.0, 100).await.unwrap(); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 19.0 ); assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 1.0 / 11.0 ); // // TEST: cancellation allows for resets // order_placer.cancel_all().await; assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 19.0 ); assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 1.0 / 11.0 ); // Process events such that the OutEvent deactivates the closed order on open_orders context .serum .consume_spot_events(&serum_market_cookie, &[order_placer.open_orders]) .await; // takes new value for bid, resets ask order_placer.bid_maker(1.0, 100).await.unwrap(); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 0.0 ); assert_eq!( order_placer .mango_serum_orders() .await .highest_placed_bid_inv, 1.0 ); // // TEST: can reset even when there's still an order on the other side // let (oid, _) = order_placer.ask(10.0, 100).await.unwrap(); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 10.0 ); order_placer.cancel(oid).await; context .serum .consume_spot_events(&serum_market_cookie, &[order_placer.open_orders]) .await; order_placer.ask(9.0, 100).await.unwrap(); assert_eq!( order_placer.mango_serum_orders().await.lowest_placed_ask, 9.0 ); Ok(()) } #[tokio::test] async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots let context = test_builder.start_default().await; // // SETUP: Create a group, accounts, market etc // let deposit_amount = 100000; let CommonSetup { serum_market_cookie, quote_token, base_token, mut order_placer, mut order_placer2, .. } = common_setup(&context, deposit_amount).await; let solana = &context.solana.clone(); let quote_bank = quote_token.bank; let base_bank = base_token.bank; let account = order_placer.account; let get_vals = |solana| async move { let account_data = get_mango_account(solana, account).await; let orders = account_data.all_serum3_orders().next().unwrap(); let base_bank = solana.get_account::(base_bank).await; let quote_bank = solana.get_account::(quote_bank).await; ( orders.base_deposits_reserved, base_bank.deposits_in_serum, orders.quote_deposits_reserved, quote_bank.deposits_in_serum, ) }; // // TEST: place a bid and ask and observe tracking // order_placer.bid_maker(0.8, 2000).await.unwrap(); order_placer.ask(1.2, 2000).await.unwrap(); assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600)); // // TEST: match partially on both sides, increasing the on-bank reserved amounts // because order_placer2 puts funds into the serum oo // order_placer2.bid_taker(1.2, 1000).await.unwrap(); context .serum .consume_spot_events( &serum_market_cookie, &[order_placer.open_orders, order_placer2.open_orders], ) .await; assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 2801)); order_placer2.settle_v2(false).await; assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600)); order_placer2.ask(0.8, 1000).await.unwrap(); context .serum .consume_spot_events( &serum_market_cookie, &[order_placer.open_orders, order_placer2.open_orders], ) .await; assert_eq!(get_vals(solana).await, (2000, 3000, 1600, 1600)); order_placer2.settle_v2(false).await; assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600)); // // TEST: Settlement updates the values // order_placer.settle_v2(false).await; assert_eq!(get_vals(solana).await, (1000, 1000, 800, 800)); Ok(()) } struct CommonSetup { group_with_tokens: GroupWithTokens, serum_market_cookie: SpotMarketCookie, quote_token: crate::program_test::mango_setup::Token, base_token: crate::program_test::mango_setup::Token, order_placer: SerumOrderPlacer, order_placer2: SerumOrderPlacer, } async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup { let admin = TestKeypair::new(); let owner = context.users[0].key; let payer = context.users[1].key; let mints = &context.mints[0..3]; let solana = &context.solana.clone(); let group_with_tokens = GroupWithTokensConfig { admin, payer, mints: mints.to_vec(), ..GroupWithTokensConfig::default() } .create(solana) .await; let group = group_with_tokens.group; let tokens = group_with_tokens.tokens.clone(); let base_token = &tokens[1]; let quote_token = &tokens[0]; // // SETUP: Create serum market // let serum_market_cookie = context .serum .list_spot_market(&base_token.mint, "e_token.mint) .await; // // SETUP: Register a serum market // let serum_market = send_tx( solana, Serum3RegisterMarketInstruction { group, admin, serum_program: context.serum.program_id, serum_market_external: serum_market_cookie.market, market_index: 0, base_bank: base_token.bank, quote_bank: quote_token.bank, payer, }, ) .await .unwrap() .serum_market; // // SETUP: Create accounts // let account = create_funded_account( &solana, group, owner, 0, &context.users[1], mints, deposit_amount, 0, ) .await; let account2 = create_funded_account( &solana, group, owner, 2, &context.users[1], mints, deposit_amount, 0, ) .await; // to have enough funds in the vaults create_funded_account( &solana, group, owner, 3, &context.users[1], mints, 10000000, 0, ) .await; let open_orders = send_tx( solana, Serum3CreateOpenOrdersInstruction { account, serum_market, owner, payer, }, ) .await .unwrap() .open_orders; let open_orders2 = send_tx( solana, Serum3CreateOpenOrdersInstruction { account: account2, serum_market, owner, payer, }, ) .await .unwrap() .open_orders; let order_placer = SerumOrderPlacer { solana: solana.clone(), serum: context.serum.clone(), account, owner: owner.clone(), serum_market, open_orders, next_client_order_id: 0, }; let order_placer2 = SerumOrderPlacer { solana: solana.clone(), serum: context.serum.clone(), account: account2, owner: owner.clone(), serum_market, open_orders: open_orders2, next_client_order_id: 100000, }; CommonSetup { group_with_tokens, serum_market_cookie, quote_token: quote_token.clone(), base_token: base_token.clone(), order_placer, order_placer2, } }