diff --git a/programs/mango-v4/src/accounts_ix/account_buyback_fees_with_mngo.rs b/programs/mango-v4/src/accounts_ix/account_buyback_fees_with_mngo.rs new file mode 100644 index 000000000..6b28381d6 --- /dev/null +++ b/programs/mango-v4/src/accounts_ix/account_buyback_fees_with_mngo.rs @@ -0,0 +1,52 @@ +use crate::error::*; +use crate::state::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct AccountBuybackFeesWithMngo<'info> { + #[account( + constraint = group.load()?.is_ix_enabled(IxGate::AccountBuybackFeesWithMngo) @ MangoError::IxIsDisabled, + constraint = group.load()?.buyback_fees() @ MangoError::SomeError + )] + pub group: AccountLoader<'info, Group>, + + #[account( + mut, + has_one = group, + constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen + // owner is checked at #1 + )] + pub account: AccountLoader<'info, MangoAccountFixed>, + pub owner: Signer<'info>, + + #[account( + mut, + has_one = group, + constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen, + address = group.load()?.buyback_fees_swap_mango_account + )] + pub dao_account: AccountLoader<'info, MangoAccountFixed>, + + #[account( + mut, + has_one = group, + constraint = mngo_bank.load()?.token_index == group.load()?.mngo_token_index, + constraint = mngo_bank.load()?.token_index != 0, // should not be unset + )] + pub mngo_bank: AccountLoader<'info, Bank>, + + /// CHECK: Oracle can have different account types + #[account(address = mngo_bank.load()?.oracle)] + pub mngo_oracle: UncheckedAccount<'info>, + + #[account( + mut, + has_one = group, + constraint = fees_bank.load()?.token_index == QUOTE_TOKEN_INDEX + )] + pub fees_bank: AccountLoader<'info, Bank>, + + /// CHECK: Oracle can have different account types + #[account(address = fees_bank.load()?.oracle)] + pub fees_oracle: UncheckedAccount<'info>, +} diff --git a/programs/mango-v4/src/accounts_ix/mod.rs b/programs/mango-v4/src/accounts_ix/mod.rs index 791b6f396..b9857f45c 100644 --- a/programs/mango-v4/src/accounts_ix/mod.rs +++ b/programs/mango-v4/src/accounts_ix/mod.rs @@ -1,3 +1,4 @@ +pub use account_buyback_fees_with_mngo::*; pub use account_close::*; pub use account_create::*; pub use account_edit::*; @@ -53,6 +54,7 @@ pub use token_register_trustless::*; pub use token_update_index_and_rate::*; pub use token_withdraw::*; +mod account_buyback_fees_with_mngo; mod account_close; mod account_create; mod account_edit; diff --git a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs new file mode 100644 index 000000000..b47a8be57 --- /dev/null +++ b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs @@ -0,0 +1,154 @@ +use anchor_lang::prelude::*; +use fixed::types::I80F48; + +use crate::accounts_zerocopy::*; +use crate::error::MangoError; +use crate::state::*; + +use crate::accounts_ix::*; + +pub fn account_buyback_fees_with_mngo( + ctx: Context, + max_buyback: u64, +) -> Result<()> { + // Cannot buyback from yourself + require_keys_neq!( + ctx.accounts.account.key(), + ctx.accounts.dao_account.key(), + MangoError::SomeError + ); + + let mut account = ctx.accounts.account.load_full_mut()?; + // account constraint #1 + require!( + account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), + MangoError::SomeError + ); + + let mut dao_account = ctx.accounts.dao_account.load_full_mut()?; + + let group = ctx.accounts.group.load()?; + + let mut mngo_bank = ctx.accounts.mngo_bank.load_mut()?; + let mut fees_bank = ctx.accounts.fees_bank.load_mut()?; + + let bonus_factor = I80F48::from_num(group.buyback_fees_mngo_bonus_factor); + let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap(); + + // quick return if nothing to buyback + let mut max_buyback = { + let dao_fees_token_position = dao_account.ensure_token_position(fees_bank.token_index)?.0; + let dao_fees_native = dao_fees_token_position.native(&fees_bank); + I80F48::from_num::(max_buyback.min(account.fixed.buyback_fees_accrued)) + .min(dao_fees_native) + }; + if max_buyback <= I80F48::ZERO { + msg!( + "nothing to buyback, (buyback_fees_accrued {})", + account.fixed.buyback_fees_accrued + ); + return Ok(()); + } + + // if mngo token position has borrows, skip buyback + let account_mngo_native = account + .token_position(mngo_bank.token_index) + .map(|tp| tp.native(&mngo_bank)) + .unwrap_or(I80F48::ZERO); + if account_mngo_native <= I80F48::ZERO { + msg!( + "account mngo token position ({} native mngo) is <= 0, nothing will be bought back", + account_mngo_native + ); + return Ok(()); + } + let (account_mngo_token_position, account_mngo_raw_token_index, _) = + account.ensure_token_position(mngo_bank.token_index)?; + + // compute max mngo to swap for fees + let mngo_oracle_price = mngo_bank.oracle_price( + &AccountInfoRef::borrow(&ctx.accounts.mngo_oracle.as_ref())?, + Some(Clock::get()?.slot), + )?; + let mngo_buyback_price = mngo_oracle_price * bonus_factor; + // mngo is exchanged at a discount + let mut max_buyback_mngo = max_buyback / mngo_buyback_price; + // buyback is restricted to account's token position + max_buyback_mngo = max_buyback_mngo.min(account_mngo_native); + max_buyback = max_buyback_mngo * mngo_buyback_price; + + // move mngo from user to dao + let (dao_mngo_token_position, dao_mngo_raw_token_index, _) = + dao_account.ensure_token_position(mngo_bank.token_index)?; + require!( + dao_mngo_token_position.indexed_position >= I80F48::ZERO, + MangoError::SomeError + ); + let in_use = mngo_bank.withdraw_without_fee( + account_mngo_token_position, + max_buyback_mngo, + now_ts, + mngo_oracle_price, + )?; + if !in_use { + account.deactivate_token_position_and_log( + account_mngo_raw_token_index, + ctx.accounts.account.key(), + ); + } + mngo_bank.deposit(dao_mngo_token_position, max_buyback_mngo, now_ts)?; + + // move fees from dao to user + let (account_fees_token_position, account_fees_raw_token_index, _) = + account.ensure_token_position(fees_bank.token_index)?; + let (dao_fees_token_position, dao_fees_raw_token_index, _) = + dao_account.ensure_token_position(fees_bank.token_index)?; + let dao_fees_native = dao_fees_token_position.native(&fees_bank); + assert!(dao_fees_native >= max_buyback); + let in_use = fees_bank.withdraw_without_fee( + dao_fees_token_position, + max_buyback, + now_ts, + mngo_oracle_price, + )?; + if !in_use { + dao_account.deactivate_token_position_and_log( + dao_fees_raw_token_index, + ctx.accounts.dao_account.key(), + ); + } + let in_use = fees_bank.deposit(account_fees_token_position, max_buyback, now_ts)?; + if !in_use { + account.deactivate_token_position_and_log( + account_fees_raw_token_index, + ctx.accounts.account.key(), + ); + } + + account.fixed.buyback_fees_accrued = account + .fixed + .buyback_fees_accrued + .saturating_sub(max_buyback.ceil().to_num::()); + msg!( + "bought back {} native fees with {} native mngo", + max_buyback, + max_buyback_mngo + ); + + // ensure dao mango account has no liabilities after we do the token swap + for ele in dao_account.all_token_positions() { + require!(!ele.indexed_position.is_negative(), MangoError::SomeError); + } + require_eq!( + dao_account.active_perp_positions().count(), + 0, + MangoError::SomeError + ); + require_eq!( + dao_account.active_serum3_orders().count(), + 0, + MangoError::SomeError + ); + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/benchmark.rs b/programs/mango-v4/src/instructions/benchmark.rs index 9974bdeeb..283100581 100644 --- a/programs/mango-v4/src/instructions/benchmark.rs +++ b/programs/mango-v4/src/instructions/benchmark.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use anchor_lang::prelude::*; use fixed::types::{I80F48, U80F48}; use solana_program::{log::sol_log_compute_units, program_memory::sol_memcmp}; diff --git a/programs/mango-v4/src/instructions/group_edit.rs b/programs/mango-v4/src/instructions/group_edit.rs index 06a4072e1..6f5cd6b65 100644 --- a/programs/mango-v4/src/instructions/group_edit.rs +++ b/programs/mango-v4/src/instructions/group_edit.rs @@ -1,9 +1,10 @@ use anchor_lang::prelude::*; -use crate::accounts_ix::*; +use crate::{accounts_ix::*, state::TokenIndex}; // use case - transfer group ownership to governance, where // admin and fast_listing_admin are PDAs +#[allow(clippy::too_many_arguments)] pub fn group_edit( ctx: Context, admin_opt: Option, @@ -12,6 +13,10 @@ pub fn group_edit( testing_opt: Option, version_opt: Option, deposit_limit_quote_opt: Option, + buyback_fees_opt: Option, + buyback_fees_bonus_factor_opt: Option, + buyback_fees_swap_mango_account_opt: Option, + mngo_token_index_opt: Option, ) -> Result<()> { let mut group = ctx.accounts.group.load_mut()?; @@ -58,5 +63,38 @@ pub fn group_edit( group.deposit_limit_quote = deposit_limit_quote; } + if let Some(buyback_fees) = buyback_fees_opt { + msg!( + "Buyback fees old {:?}, new {:?}", + group.buyback_fees, + buyback_fees + ); + group.buyback_fees = u8::from(buyback_fees); + } + if let Some(buyback_fees_mngo_bonus_factor) = buyback_fees_bonus_factor_opt { + msg!( + "Buyback fees mngo bonus factor old {:?}, new {:?}", + group.buyback_fees_mngo_bonus_factor, + buyback_fees_mngo_bonus_factor + ); + group.buyback_fees_mngo_bonus_factor = buyback_fees_mngo_bonus_factor; + } + if let Some(buyback_fees_swap_mango_account) = buyback_fees_swap_mango_account_opt { + msg!( + "Buyback fees swap mango account old {:?}, new {:?}", + group.buyback_fees_swap_mango_account, + buyback_fees_swap_mango_account + ); + group.buyback_fees_swap_mango_account = buyback_fees_swap_mango_account; + } + if let Some(mngo_token_index) = mngo_token_index_opt { + msg!( + "Mngo token index old {:?}, new {:?}", + group.mngo_token_index, + mngo_token_index + ); + group.mngo_token_index = mngo_token_index; + } + Ok(()) } diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 791b6f396..b9857f45c 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -1,3 +1,4 @@ +pub use account_buyback_fees_with_mngo::*; pub use account_close::*; pub use account_create::*; pub use account_edit::*; @@ -53,6 +54,7 @@ pub use token_register_trustless::*; pub use token_update_index_and_rate::*; pub use token_withdraw::*; +mod account_buyback_fees_with_mngo; mod account_close; mod account_create; mod account_edit; diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index d96f7dc9d..bbca5c49f 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -51,6 +51,7 @@ pub mod mango_v4 { Ok(()) } + #[allow(clippy::too_many_arguments)] pub fn group_edit( ctx: Context, admin_opt: Option, @@ -59,6 +60,10 @@ pub mod mango_v4 { testing_opt: Option, version_opt: Option, deposit_limit_quote_opt: Option, + buyback_fees_opt: Option, + buyback_fees_bonus_factor_opt: Option, + buyback_fees_swap_mango_account_opt: Option, + mngo_token_index_opt: Option, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::group_edit( @@ -69,6 +74,10 @@ pub mod mango_v4 { testing_opt, version_opt, deposit_limit_quote_opt, + buyback_fees_opt, + buyback_fees_bonus_factor_opt, + buyback_fees_swap_mango_account_opt, + mngo_token_index_opt, )?; Ok(()) } @@ -270,6 +279,15 @@ pub mod mango_v4 { Ok(()) } + pub fn account_buyback_fees_with_mngo( + ctx: Context, + max_buyback: u64, + ) -> Result<()> { + #[cfg(feature = "enable-gpl")] + instructions::account_buyback_fees_with_mngo(ctx, max_buyback)?; + Ok(()) + } + // todo: // ckamm: generally, using an I80F48 arg will make it harder to call // because generic anchor clients won't know how to deal with it diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index 660c77ea3..dbd2b05c2 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -20,7 +20,9 @@ pub struct Group { // TODO: unused, use case - listing shit tokens with conservative parameters (mostly defaults) pub fast_listing_admin: Pubkey, - pub padding: [u8; 4], + // This is the token index of the mngo token listed on the group + pub mngo_token_index: TokenIndex, + pub padding: [u8; 2], pub insurance_vault: Pubkey, pub insurance_mint: Pubkey, @@ -31,7 +33,11 @@ pub struct Group { pub version: u8, - pub padding2: [u8; 5], + // Buyback fees with Mngo: allow exchanging fees with mngo at a bonus + pub buyback_fees: u8, + // Buyback fees with Mngo: how much should the bonus be, + // e.g. a bonus factor of 1.2 means 120$ worth fees could be swapped for mngo worth 100$ at current market price + pub buyback_fees_mngo_bonus_factor: f32, pub address_lookup_tables: [Pubkey; 20], @@ -45,16 +51,29 @@ pub struct Group { // 0 is chosen as enabled, becase we want to start out with all ixs enabled, 1 is disabled pub ix_gate: u128, - pub reserved: [u8; 1864], + // Buyback fees with Mngo: + // A mango account which would be counter party for settling fees with mngo + // This ensures that the system doesn't have a net deficit of tokens + // The workflow should be something like this + // - the dao deposits quote tokens in its respective mango account + // - the user deposits some mngo tokens in his mango account + // - the user then claims quote for mngo at a bonus rate + pub buyback_fees_swap_mango_account: Pubkey, + + pub reserved: [u8; 1832], } const_assert_eq!( size_of::(), - 32 + 4 + 32 * 2 + 4 + 32 * 2 + 3 + 5 + 20 * 32 + 32 + 8 + 16 + 1864 + 32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 1832 ); const_assert_eq!(size_of::(), 2736); const_assert_eq!(size_of::() % 8, 0); impl Group { + pub fn buyback_fees(&self) -> bool { + self.buyback_fees == 1 + } + pub fn is_testing(&self) -> bool { self.testing == 1 } @@ -139,6 +158,7 @@ pub enum IxGate { TokenRegisterTrustless = 45, TokenUpdateIndexAndRate = 46, TokenWithdraw = 47, + AccountBuybackFeesWithMngo = 48, } // note: using creator instead of admin, since admin can be changed diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 73d653577..c12889663 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -87,7 +87,9 @@ pub struct MangoAccount { pub frozen_until: u64, - pub reserved: [u8; 232], + pub buyback_fees_accrued: u64, + + pub reserved: [u8; 224], // dynamic pub header_version: u8, @@ -122,7 +124,8 @@ impl MangoAccount { net_deposits: 0, health_region_begin_init_health: 0, frozen_until: 0, - reserved: [0; 232], + buyback_fees_accrued: 0, + reserved: [0; 224], header_version: DEFAULT_MANGO_ACCOUNT_VERSION, padding3: Default::default(), padding4: Default::default(), @@ -204,9 +207,13 @@ pub struct MangoAccountFixed { pub perp_spot_transfers: i64, pub health_region_begin_init_health: i64, pub frozen_until: u64, - pub reserved: [u8; 232], + pub buyback_fees_accrued: u64, + pub reserved: [u8; 224], } -const_assert_eq!(size_of::(), 32 * 4 + 8 + 3 * 8 + 8 + 232); +const_assert_eq!( + size_of::(), + 32 * 4 + 8 + 3 * 8 + 8 + 8 + 224 +); const_assert_eq!(size_of::(), 400); const_assert_eq!(size_of::() % 8, 0); @@ -891,13 +898,15 @@ impl< perp_market: &mut PerpMarket, fill: &FillEvent, ) -> Result<()> { - let pa = self.perp_position_mut(perp_market_index)?; - pa.settle_funding(perp_market); - let side = fill.taker_side().invert_side(); let (base_change, quote_change) = fill.base_quote_change(side); let quote = I80F48::from(perp_market.quote_lot_size) * I80F48::from(quote_change); let fees = quote.abs() * I80F48::from_num(fill.maker_fee); + if fees.is_positive() { + self.fixed_mut().buyback_fees_accrued += fees.floor().to_num::(); + } + let pa = self.perp_position_mut(perp_market_index)?; + pa.settle_funding(perp_market); pa.record_trading_fee(fees); pa.record_trade(perp_market, base_change, quote); diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index 91a47292b..8f149ef69 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -1,4 +1,4 @@ -use crate::state::{MangoAccountRefMut, PerpPosition}; +use crate::state::MangoAccountRefMut; use crate::{ error::*, state::{orderbook::bookside::*, EventQueue, PerpMarket}, @@ -58,16 +58,17 @@ impl<'a> Orderbook<'a> { let post_only = order.is_post_only(); let mut post_target = order.post_target(); let (price_lots, price_data) = order.price(now_ts, oracle_price_lots, self)?; - let perp_position = mango_account.perp_position_mut(market.perp_market_index)?; // generate new order id let order_id = market.gen_order_id(side, price_data); // IOC orders have a fee penalty applied regardless of match if order.needs_penalty_fee() { - apply_penalty(market, perp_position)?; + apply_penalty(market, mango_account)?; } + let perp_position = mango_account.perp_position_mut(market.perp_market_index)?; + // Iterate through book and match against this new order. // // Any changes to matching orders on the other side of the book are collected in @@ -164,7 +165,7 @@ impl<'a> Orderbook<'a> { // realized when the fill event gets executed if total_quote_lots_taken > 0 || total_base_lots_taken > 0 { perp_position.add_taker_trade(side, total_base_lots_taken, total_quote_lots_taken); - apply_fees(market, perp_position, total_quote_lots_taken)?; + apply_fees(market, mango_account, total_quote_lots_taken)?; } // Apply changes to matched asks (handles invalidate on delete!) @@ -350,7 +351,7 @@ impl<'a> Orderbook<'a> { /// both the maker and taker fees. fn apply_fees( market: &mut PerpMarket, - perp_position: &mut PerpPosition, + account: &mut MangoAccountRefMut, quote_lots: i64, ) -> Result<()> { let quote_native = I80F48::from_num(market.quote_lot_size * quote_lots); @@ -362,6 +363,8 @@ fn apply_fees( require_gte!(taker_fees, 0); // The maker fees apply to the maker's account only when the fill event is consumed. + account.fixed.buyback_fees_accrued += taker_fees.floor().to_num::(); + let perp_position = account.perp_position_mut(market.perp_market_index)?; perp_position.record_trading_fee(taker_fees); perp_position.taker_volume += taker_fees.to_num::(); @@ -374,8 +377,11 @@ fn apply_fees( } /// Applies a fixed penalty fee to the account, and update the market's fees_accrued -fn apply_penalty(market: &mut PerpMarket, perp_position: &mut PerpPosition) -> Result<()> { +fn apply_penalty(market: &mut PerpMarket, account: &mut MangoAccountRefMut) -> Result<()> { let fee_penalty = I80F48::from_num(market.fee_penalty); + account.fixed.buyback_fees_accrued += fee_penalty.floor().to_num::(); + + let perp_position = account.perp_position_mut(market.perp_market_index)?; perp_position.record_trading_fee(fee_penalty); market.fees_accrued += fee_penalty; Ok(()) diff --git a/programs/mango-v4/tests/cases/mod.rs b/programs/mango-v4/tests/cases/mod.rs index 85c711677..858e3dc66 100644 --- a/programs/mango-v4/tests/cases/mod.rs +++ b/programs/mango-v4/tests/cases/mod.rs @@ -17,6 +17,7 @@ mod test_basic; mod test_benchmark; mod test_borrow_limits; mod test_delegate; +mod test_fees_buyback_with_mngo; mod test_health_compute; mod test_health_region; mod test_ix_gate_set; diff --git a/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs b/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs new file mode 100644 index 000000000..ac6eec4c5 --- /dev/null +++ b/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs @@ -0,0 +1,196 @@ +use super::*; + +#[tokio::test] +async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..2]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let deposit_amount = 100_000_000; + let account_0 = create_funded_account( + solana, + group, + owner, + 0, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + let account_1 = create_funded_account( + solana, + group, + owner, + 1, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + let account_2 = create_funded_account( + solana, + group, + owner, + 2, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + + // + // Create a perp market + // + let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx( + solana, + PerpCreateMarketInstruction { + group, + admin, + payer, + perp_market_index: 0, + quote_lot_size: 10, + base_lot_size: 100, + maint_base_asset_weight: 0.975, + init_base_asset_weight: 0.95, + maint_base_liab_weight: 1.025, + init_base_liab_weight: 1.05, + base_liquidation_fee: 0.012, + maker_fee: -0.01, + taker_fee: 0.02, + settle_pnl_limit_factor: -1.0, + settle_pnl_limit_window_size_ts: 24 * 60 * 60, + ..PerpCreateMarketInstruction::with_new_book_and_queue(solana, &tokens[0]).await + }, + ) + .await + .unwrap(); + + let price_lots = { + let perp_market = solana.get_account::(perp_market).await; + perp_market.native_price_to_lot(I80F48::from(1)) + }; + + // + // Place a bid, corresponding ask, and consume event + // + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 10, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 5, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 10, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 6, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + + // + // Test: Account buyback fees accrued with mngo + // + send_tx( + solana, + GroupEditFeeParameters { + group, + admin, + fees_mngo_token_index: 1 as TokenIndex, + fees_swap_mango_account: account_2, + fees_mngo_bonus_factor: 1.2, + }, + ) + .await + .unwrap(); + + let mango_account_1 = solana.get_account::(account_1).await; + let before_fees_accrued = mango_account_1.buyback_fees_accrued; + let fees_token_position_before = + mango_account_1.tokens[0].native(&solana.get_account::(tokens[0].bank).await); + let mngo_token_position_before = + mango_account_1.tokens[1].native(&solana.get_account::(tokens[1].bank).await); + send_tx( + solana, + AccountBuybackFeesWithMngo { + owner, + account: account_1, + mngo_bank: tokens[1].bank, + fees_bank: tokens[0].bank, + }, + ) + .await + .unwrap(); + let mango_account_1 = solana.get_account::(account_1).await; + let after_fees_accrued = solana + .get_account::(account_1) + .await + .buyback_fees_accrued; + let fees_token_position_after = + mango_account_1.tokens[0].native(&solana.get_account::(tokens[0].bank).await); + let mngo_token_position_after = + mango_account_1.tokens[1].native(&solana.get_account::(tokens[1].bank).await); + + assert_eq!(before_fees_accrued - after_fees_accrued, 19); + + // token[1] swapped at discount for token[0] + assert!( + (fees_token_position_after - fees_token_position_before) - I80F48::from_num(20) + < I80F48::from_num(0.000001) + ); + assert!( + (mngo_token_position_before - mngo_token_position_after) - I80F48::from_num(16.666666) + < I80F48::from_num(0.000001) + ); + + Ok(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 28e40e962..975b979d6 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1503,6 +1503,59 @@ impl ClientInstruction for GroupCreateInstruction { } } +fn group_edit_instruction_default() -> mango_v4::instruction::GroupEdit { + mango_v4::instruction::GroupEdit { + admin_opt: None, + fast_listing_admin_opt: None, + security_admin_opt: None, + testing_opt: None, + version_opt: None, + deposit_limit_quote_opt: None, + buyback_fees_opt: None, + buyback_fees_bonus_factor_opt: None, + buyback_fees_swap_mango_account_opt: None, + mngo_token_index_opt: None, + } +} + +pub struct GroupEditFeeParameters { + pub group: Pubkey, + pub admin: TestKeypair, + pub fees_mngo_bonus_factor: f32, + pub fees_mngo_token_index: TokenIndex, + pub fees_swap_mango_account: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for GroupEditFeeParameters { + type Accounts = mango_v4::accounts::GroupEdit; + type Instruction = mango_v4::instruction::GroupEdit; + async fn to_instruction( + &self, + _account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + buyback_fees_opt: Some(true), + buyback_fees_bonus_factor_opt: Some(self.fees_mngo_bonus_factor), + buyback_fees_swap_mango_account_opt: Some(self.fees_swap_mango_account), + mngo_token_index_opt: Some(self.fees_mngo_token_index), + ..group_edit_instruction_default() + }; + + let accounts = Self::Accounts { + group: self.group, + admin: self.admin.pubkey(), + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.admin] + } +} + pub struct IxGateSetInstruction { pub group: Pubkey, pub admin: TestKeypair, @@ -1765,6 +1818,55 @@ impl ClientInstruction for AccountCloseInstruction { } } +pub struct AccountBuybackFeesWithMngo { + pub owner: TestKeypair, + pub account: Pubkey, + pub mngo_bank: Pubkey, + pub fees_bank: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for AccountBuybackFeesWithMngo { + type Accounts = mango_v4::accounts::AccountBuybackFeesWithMngo; + type Instruction = mango_v4::instruction::AccountBuybackFeesWithMngo; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + max_buyback: u64::MAX, + }; + + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + let group = account_loader + .load::(&account.fixed.group) + .await + .unwrap(); + let mngo_bank: Bank = account_loader.load(&self.mngo_bank).await.unwrap(); + let fees_bank: Bank = account_loader.load(&self.fees_bank).await.unwrap(); + let accounts = Self::Accounts { + group: account.fixed.group, + owner: self.owner.pubkey(), + account: self.account, + dao_account: group.buyback_fees_swap_mango_account, + mngo_bank: self.mngo_bank, + mngo_oracle: mngo_bank.oracle, + fees_bank: self.fees_bank, + fees_oracle: fees_bank.oracle, + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.owner] + } +} + pub struct Serum3RegisterMarketInstruction { pub group: Pubkey, pub admin: TestKeypair, diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index b064d941f..120cbc541 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -475,7 +475,7 @@ export class MintInfo { obj.vaults, obj.oracle, obj.registrationTime, - obj.groupInsuranceFund, + obj.groupInsuranceFund == 1, ); } @@ -488,7 +488,7 @@ export class MintInfo { public vaults: PublicKey[], public oracle: PublicKey, public registrationTime: BN, - public groupInsuranceFund: number, + public groupInsuranceFund: boolean, ) {} public firstBank(): PublicKey { diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index a29dc481b..55fd8fcf4 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -32,13 +32,18 @@ export class Group { groupNum: number; admin: PublicKey; fastListingAdmin: PublicKey; - securityAdmin: PublicKey; + feesMngoTokenIndex: number; insuranceMint: PublicKey; insuranceVault: PublicKey; testing: number; version: number; - ixGate: BN; + feesPayWithMngo: number; + feesMngoBonusFactor: number; addressLookupTables: PublicKey[]; + securityAdmin: PublicKey; + depositLimitQuote: BN; + ixGate: BN; + feesSwapMangoAccount: PublicKey; }, ): Group { return new Group( @@ -47,13 +52,18 @@ export class Group { obj.groupNum, obj.admin, obj.fastListingAdmin, - obj.securityAdmin, + obj.feesMngoTokenIndex as TokenIndex, obj.insuranceMint, obj.insuranceVault, obj.testing, obj.version, - obj.ixGate, + obj.feesPayWithMngo == 1, + obj.feesMngoBonusFactor, obj.addressLookupTables, + obj.securityAdmin, + obj.depositLimitQuote, + obj.ixGate, + obj.feesSwapMangoAccount, [], // addressLookupTablesList new Map(), // banksMapByName new Map(), // banksMapByMint @@ -76,13 +86,18 @@ export class Group { public groupNum: number, public admin: PublicKey, public fastListingAdmin: PublicKey, - public securityAdmin: PublicKey, + public feesMngoTokenIndex: TokenIndex, public insuranceMint: PublicKey, public insuranceVault: PublicKey, public testing: number, public version: number, - public ixGate: BN, + public feesPayWithMngo: boolean, + public feesMngoBonusFactor: number, public addressLookupTables: PublicKey[], + public securityAdmin: PublicKey, + public depositLimitQuote, + public ixGate: BN, + public feesSwapMangoAccount: PublicKey, public addressLookupTablesList: AddressLookupTableAccount[], public banksMapByName: Map, public banksMapByMint: Map, diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index bc2afb20f..5f604c281 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -32,6 +32,7 @@ export class MangoAccount { perpSpotTransfers: BN; healthRegionBeginInitHealth: BN; frozenUntil: BN; + buybackFeesAccrued: BN; headerVersion: number; tokens: unknown; serum3: unknown; @@ -52,6 +53,7 @@ export class MangoAccount { obj.perpSpotTransfers, obj.healthRegionBeginInitHealth, obj.frozenUntil, + obj.buybackFeesAccrued, obj.headerVersion, obj.tokens as TokenPositionDto[], obj.serum3 as Serum3PositionDto[], @@ -74,6 +76,7 @@ export class MangoAccount { public perpSpotTransfers: BN, public healthRegionBeginInitHealth: BN, public frozenUntil: BN, + public buybackFeesAccrued: BN, public headerVersion: number, tokens: TokenPositionDto[], serum3: Serum3PositionDto[], diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 30291dd40..af1f3ba77 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -158,6 +158,10 @@ export class MangoClient { testing?: number, version?: number, depositLimitQuote?: BN, + feesPayWithMngo?: boolean, + feesMngoBonusRate?: number, + feesSwapMangoAccount?: PublicKey, + feesMngoTokenIndex?: TokenIndex, ): Promise { const ix = await this.program.methods .groupEdit( @@ -167,6 +171,10 @@ export class MangoClient { testing ?? null, version ?? null, depositLimitQuote !== undefined ? depositLimitQuote : null, + feesPayWithMngo ?? null, + feesMngoBonusRate ?? null, + feesSwapMangoAccount ?? null, + feesMngoTokenIndex ?? null, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index e3506db76..f410a04ba 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -287,6 +287,8 @@ export function buildIxGate(p: IxGateParams): BN { toggleIx(ixGate, p, 'TokenRegisterTrustless', 45); toggleIx(ixGate, p, 'TokenUpdateIndexAndRate', 46); toggleIx(ixGate, p, 'TokenWithdraw', 47); + toggleIx(ixGate, p, 'AccountSettleFeesWithMngo', 48); return ixGate; } + diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 81cd7f174..1f8628799 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -144,6 +144,30 @@ export type MangoV4 = { "type": { "option": "u64" } + }, + { + "name": "feesPayWithMngoOpt", + "type": { + "option": "bool" + } + }, + { + "name": "feesMngoBonusFactorOpt", + "type": { + "option": "f32" + } + }, + { + "name": "feesSwapMangoAccountOpt", + "type": { + "option": "publicKey" + } + }, + { + "name": "feesMngoTokenIndexOpt", + "type": { + "option": "u16" + } } ] }, @@ -1094,6 +1118,57 @@ export type MangoV4 = { } ] }, + { + "name": "accountBuybackFeesWithMngo", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "daoAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "mngoBank", + "isMut": true, + "isSigner": false + }, + { + "name": "mngoOracle", + "isMut": false, + "isSigner": false + }, + { + "name": "feesBank", + "isMut": true, + "isSigner": false + }, + { + "name": "feesOracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxBuyback", + "type": "u64" + } + ] + }, { "name": "stubOracleCreate", "accounts": [ @@ -3947,12 +4022,16 @@ export type MangoV4 = { "name": "fastListingAdmin", "type": "publicKey" }, + { + "name": "feesMngoTokenIndex", + "type": "u16" + }, { "name": "padding", "type": { "array": [ "u8", - 4 + 2 ] } }, @@ -3977,13 +4056,12 @@ export type MangoV4 = { "type": "u8" }, { - "name": "padding2", - "type": { - "array": [ - "u8", - 5 - ] - } + "name": "feesPayWithMngo", + "type": "u8" + }, + { + "name": "feesMngoBonusFactor", + "type": "f32" }, { "name": "addressLookupTables", @@ -4006,12 +4084,16 @@ export type MangoV4 = { "name": "ixGate", "type": "u128" }, + { + "name": "feesSwapMangoAccount", + "type": "publicKey" + }, { "name": "reserved", "type": { "array": [ "u8", - 1864 + 1832 ] } } @@ -4104,12 +4186,16 @@ export type MangoV4 = { "name": "frozenUntil", "type": "u64" }, + { + "name": "discountBuybackFeesAccrued", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 232 + 224 ] } }, @@ -5679,12 +5765,16 @@ export type MangoV4 = { "name": "frozenUntil", "type": "u64" }, + { + "name": "discountBuybackFeesAccrued", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 232 + 224 ] } } @@ -6682,6 +6772,9 @@ export type MangoV4 = { }, { "name": "TokenWithdraw" + }, + { + "name": "AccountBuybackFeesWithMngo" } ] } @@ -8490,6 +8583,30 @@ export const IDL: MangoV4 = { "type": { "option": "u64" } + }, + { + "name": "feesPayWithMngoOpt", + "type": { + "option": "bool" + } + }, + { + "name": "feesMngoBonusFactorOpt", + "type": { + "option": "f32" + } + }, + { + "name": "feesSwapMangoAccountOpt", + "type": { + "option": "publicKey" + } + }, + { + "name": "feesMngoTokenIndexOpt", + "type": { + "option": "u16" + } } ] }, @@ -9440,6 +9557,57 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "accountBuybackFeesWithMngo", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "daoAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "mngoBank", + "isMut": true, + "isSigner": false + }, + { + "name": "mngoOracle", + "isMut": false, + "isSigner": false + }, + { + "name": "feesBank", + "isMut": true, + "isSigner": false + }, + { + "name": "feesOracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxBuyback", + "type": "u64" + } + ] + }, { "name": "stubOracleCreate", "accounts": [ @@ -12293,12 +12461,16 @@ export const IDL: MangoV4 = { "name": "fastListingAdmin", "type": "publicKey" }, + { + "name": "feesMngoTokenIndex", + "type": "u16" + }, { "name": "padding", "type": { "array": [ "u8", - 4 + 2 ] } }, @@ -12323,13 +12495,12 @@ export const IDL: MangoV4 = { "type": "u8" }, { - "name": "padding2", - "type": { - "array": [ - "u8", - 5 - ] - } + "name": "feesPayWithMngo", + "type": "u8" + }, + { + "name": "feesMngoBonusFactor", + "type": "f32" }, { "name": "addressLookupTables", @@ -12352,12 +12523,16 @@ export const IDL: MangoV4 = { "name": "ixGate", "type": "u128" }, + { + "name": "feesSwapMangoAccount", + "type": "publicKey" + }, { "name": "reserved", "type": { "array": [ "u8", - 1864 + 1832 ] } } @@ -12450,12 +12625,16 @@ export const IDL: MangoV4 = { "name": "frozenUntil", "type": "u64" }, + { + "name": "discountBuybackFeesAccrued", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 232 + 224 ] } }, @@ -14025,12 +14204,16 @@ export const IDL: MangoV4 = { "name": "frozenUntil", "type": "u64" }, + { + "name": "discountBuybackFeesAccrued", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 232 + 224 ] } } @@ -15028,6 +15211,9 @@ export const IDL: MangoV4 = { }, { "name": "TokenWithdraw" + }, + { + "name": "AccountBuybackFeesWithMngo" } ] }