Serum settle funds V2: fees can go to users (#484)

Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
Christian Kamm 2023-03-03 14:04:45 +01:00 committed by GitHub
parent 1950d8c84a
commit 252210d194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 977 additions and 218 deletions

View File

@ -60,3 +60,21 @@ pub struct Serum3SettleFunds<'info> {
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct Serum3SettleFundsV2Extra<'info> {
/// CHECK: The oracle can be one of several different account types and the pubkey is checked in the parent
pub quote_oracle: UncheckedAccount<'info>,
/// CHECK: The oracle can be one of several different account types and the pubkey is checked in the parent
pub base_oracle: UncheckedAccount<'info>,
}
#[derive(Accounts)]
pub struct Serum3SettleFundsV2<'info> {
pub v1: Serum3SettleFunds<'info>,
#[account(
constraint = v2.quote_oracle.key() == v1.quote_bank.load()?.oracle,
constraint = v2.base_oracle.key() == v1.base_bank.load()?.oracle,
)]
pub v2: Serum3SettleFundsV2Extra<'info>,
}

View File

@ -158,6 +158,7 @@ pub fn serum3_liq_force_cancel_orders(
after_quote_vault,
&after_oo,
Some(&mut health_cache),
true,
)?;
//
@ -201,6 +202,7 @@ fn cpi_settle_funds(ctx: &Serum3LiqForceCancelOrders) -> Result<()> {
user_quote_wallet: ctx.quote_vault.to_account_info(),
vault_signer: ctx.market_vault_signer.to_account_info(),
token_program: ctx.token_program.to_account_info(),
rebates_quote_wallet: ctx.quote_vault.to_account_info(),
}
.call(&group)
}

View File

@ -424,22 +424,26 @@ pub fn apply_settle_changes(
after_quote_vault: u64,
after_oo: &OpenOrdersSlim,
health_cache: Option<&mut HealthCache>,
fees_to_dao: bool,
) -> Result<()> {
// Example: rebates go from 100 -> 10. That means we credit 90 in fees.
let received_fees = before_oo
.native_rebates()
.saturating_sub(after_oo.native_rebates());
quote_bank.collected_fees_native += I80F48::from(received_fees);
let mut received_fees = 0;
if fees_to_dao {
// Example: rebates go from 100 -> 10. That means we credit 90 in fees.
received_fees = before_oo
.native_rebates()
.saturating_sub(after_oo.native_rebates());
quote_bank.collected_fees_native += I80F48::from(received_fees);
// Ideally we could credit buyback_fees at the current value of the received fees,
// but the settle_funds instruction currently doesn't receive the oracle account
// that would be needed for it.
if quote_bank.token_index == QUOTE_TOKEN_INDEX {
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
account
.fixed
.expire_buyback_fees(now_ts, group.buyback_fees_expiry_interval);
account.fixed.accrue_buyback_fees(received_fees);
// Ideally we could credit buyback_fees at the current value of the received fees,
// but the settle_funds instruction currently doesn't receive the oracle account
// that would be needed for it.
if quote_bank.token_index == QUOTE_TOKEN_INDEX {
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
account
.fixed
.expire_buyback_fees(now_ts, group.buyback_fees_expiry_interval);
account.fixed.accrue_buyback_fees(received_fees);
}
}
// Don't count the referrer rebate fees as part of the vault change that should be

View File

@ -16,17 +16,21 @@ use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
///
/// There will be free funds on open_orders when an order was triggered.
///
pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
let serum_market = ctx.accounts.serum_market.load()?;
pub fn serum3_settle_funds<'info>(
accounts: &mut Serum3SettleFunds<'info>,
v2: Option<&mut Serum3SettleFundsV2Extra<'info>>,
fees_to_dao: bool,
) -> Result<()> {
let serum_market = accounts.serum_market.load()?;
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
let account = accounts.account.load_full()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
account.fixed.is_owner_or_delegate(accounts.owner.key()),
MangoError::SomeError
);
@ -35,23 +39,23 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
account
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
== accounts.open_orders.key(),
MangoError::SomeError
);
// Validate banks and vaults #3
let quote_bank = ctx.accounts.quote_bank.load()?;
let quote_bank = accounts.quote_bank.load()?;
require!(
quote_bank.vault == ctx.accounts.quote_vault.key(),
quote_bank.vault == accounts.quote_vault.key(),
MangoError::SomeError
);
require!(
quote_bank.token_index == serum_market.quote_token_index,
MangoError::SomeError
);
let base_bank = ctx.accounts.base_bank.load()?;
let base_bank = accounts.base_bank.load()?;
require!(
base_bank.vault == ctx.accounts.base_vault.key(),
base_bank.vault == accounts.base_vault.key(),
MangoError::SomeError
);
require!(
@ -65,14 +69,14 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
//
let before_oo;
{
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
let open_orders = load_open_orders_ref(accounts.open_orders.as_ref())?;
before_oo = OpenOrdersSlim::from_oo(&open_orders);
let mut account = ctx.accounts.account.load_full_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let mut account = accounts.account.load_full_mut()?;
let mut base_bank = accounts.base_bank.load_mut()?;
let mut quote_bank = accounts.quote_bank.load_mut()?;
charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
&accounts.group.key(),
&accounts.account.key(),
serum_market.market_index,
&mut base_bank,
&mut quote_bank,
@ -84,32 +88,32 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
//
// Settle
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
let before_base_vault = accounts.base_vault.amount;
let before_quote_vault = accounts.quote_vault.amount;
cpi_settle_funds(ctx.accounts)?;
cpi_settle_funds(accounts)?;
//
// After-settle tracking
//
let after_oo = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let oo_ai = &accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
OpenOrdersSlim::from_oo(&open_orders)
};
ctx.accounts.base_vault.reload()?;
ctx.accounts.quote_vault.reload()?;
let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount;
accounts.base_vault.reload()?;
accounts.quote_vault.reload()?;
let after_base_vault = accounts.base_vault.amount;
let after_quote_vault = accounts.quote_vault.amount;
let mut account = ctx.accounts.account.load_full_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let group = ctx.accounts.group.load()?;
let mut account = accounts.account.load_full_mut()?;
let mut base_bank = accounts.base_bank.load_mut()?;
let mut quote_bank = accounts.quote_bank.load_mut()?;
let group = accounts.group.load()?;
apply_settle_changes(
&group,
ctx.accounts.account.key(),
accounts.account.key(),
&mut account.borrow_mut(),
&mut base_bank,
&mut quote_bank,
@ -121,11 +125,12 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
after_quote_vault,
&after_oo,
None,
fees_to_dao,
)?;
emit!(Serum3OpenOrdersBalanceLogV2 {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
mango_group: accounts.group.key(),
mango_account: accounts.account.key(),
market_index: serum_market.market_index,
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
@ -211,7 +216,7 @@ pub fn charge_loan_origination_fees(
Ok(())
}
fn cpi_settle_funds(ctx: &Serum3SettleFunds) -> Result<()> {
fn cpi_settle_funds<'info>(ctx: &Serum3SettleFunds<'info>) -> Result<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;
serum3_cpi::SettleFunds {
@ -225,6 +230,7 @@ fn cpi_settle_funds(ctx: &Serum3SettleFunds) -> Result<()> {
user_quote_wallet: ctx.quote_vault.to_account_info(),
vault_signer: ctx.market_vault_signer.to_account_info(),
token_program: ctx.token_program.to_account_info(),
rebates_quote_wallet: ctx.quote_vault.to_account_info(),
}
.call(&group)
}

View File

@ -464,9 +464,27 @@ pub mod mango_v4 {
Ok(())
}
/// Settles all free funds from the OpenOrders account into the MangoAccount.
///
/// Any serum "referrer rebates" (ui fees) are considered Mango fees.
pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_settle_funds(ctx)?;
instructions::serum3_settle_funds(ctx.accounts, None, true)?;
Ok(())
}
/// Like Serum3SettleFunds, but `fees_to_dao` determines if referrer rebates are considered fees
/// or are credited to the MangoAccount.
pub fn serum3_settle_funds_v2(
ctx: Context<Serum3SettleFundsV2>,
fees_to_dao: bool,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_settle_funds(
&mut ctx.accounts.v1,
Some(&mut ctx.accounts.v2),
fees_to_dao,
)?;
Ok(())
}

View File

@ -116,7 +116,11 @@ pub fn load_open_orders_ref<'a>(
}
pub fn load_open_orders(acc: &impl AccountReader) -> Result<&serum_dex::state::OpenOrders> {
Ok(bytemuck::from_bytes(strip_dex_padding(acc.data())?))
load_open_orders_bytes(acc.data())
}
pub fn load_open_orders_bytes(bytes: &[u8]) -> Result<&serum_dex::state::OpenOrders> {
Ok(bytemuck::from_bytes(strip_dex_padding(bytes)?))
}
pub fn pubkey_from_u64_array(d: [u64; 4]) -> Pubkey {
@ -226,6 +230,8 @@ pub struct SettleFunds<'info> {
pub vault_signer: AccountInfo<'info>,
/// CHECK: cpi
pub token_program: AccountInfo<'info>,
/// CHECK: cpi
pub rebates_quote_wallet: AccountInfo<'info>,
}
impl<'a> SettleFunds<'a> {
@ -244,7 +250,7 @@ impl<'a> SettleFunds<'a> {
AccountMeta::new(*self.user_quote_wallet.key, false),
AccountMeta::new_readonly(*self.vault_signer.key, false),
AccountMeta::new_readonly(*self.token_program.key, false),
AccountMeta::new(*self.user_quote_wallet.key, false),
AccountMeta::new(*self.rebates_quote_wallet.key, false),
],
};
@ -256,10 +262,10 @@ impl<'a> SettleFunds<'a> {
self.base_vault,
self.quote_vault,
self.user_base_wallet,
self.user_quote_wallet.clone(),
self.user_quote_wallet,
self.vault_signer,
self.token_program,
self.user_quote_wallet,
self.rebates_quote_wallet,
];
let seeds = group_seeds!(group);

View File

@ -1,6 +1,7 @@
use super::*;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::{instructions::OpenOrdersSlim, serum3_cpi::load_open_orders_bytes};
use std::sync::Arc;
struct SerumOrderPlacer {
@ -119,6 +120,20 @@ impl SerumOrderPlacer {
.unwrap();
}
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
@ -127,6 +142,15 @@ impl SerumOrderPlacer {
.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]
@ -317,16 +341,455 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
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_bank,
base_bank,
mut order_placer,
mut order_placer2,
..
} = common_setup(&context, deposit_amount).await;
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(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(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::<Bank>(quote_bank)
.await
.collected_fees_native;
// account2 has an order on the book
order_placer2.bid(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::<Bank>(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::<Bank>(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::<Bank>(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::<MangoAccount>(account).await;
assert_eq!(
account_data.buyback_fees_accrued_current,
serum_fee(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_bank,
base_bank,
mut order_placer,
mut order_placer2,
..
} = common_setup(&context, deposit_amount).await;
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::<Bank>(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(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::<Bank>(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 {
serum_market_cookie,
quote_bank,
base_bank,
mut order_placer,
mut order_placer2,
..
} = common_setup(&context, deposit_amount).await;
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::<Bank>(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(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::<Bank>(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_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_bank,
base_bank,
mut order_placer,
mut order_placer2,
..
} = common_setup(&context, deposit_amount).await;
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::<Bank>(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(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::<Bank>(quote_bank)
.await
.collected_fees_native;
assert!(assert_equal(
quote_fees_end - quote_fees_start,
lof as f64,
0.1
));
Ok(())
}
struct CommonSetup {
_group_with_tokens: GroupWithTokens,
serum_market_cookie: SpotMarketCookie,
quote_bank: Pubkey,
base_bank: Pubkey,
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];
//
// SETUP: Create a group and an account
//
let solana = &context.solana.clone();
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
let group_with_tokens = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
@ -334,6 +797,8 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
}
.create(solana)
.await;
let group = group_with_tokens.group;
let tokens = group_with_tokens.tokens.clone();
let base_token = &tokens[1];
let base_bank = base_token.bank;
let quote_token = &tokens[0];
@ -370,7 +835,6 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
//
// SETUP: Create accounts
//
let deposit_amount = 180000;
let account = create_funded_account(
&solana,
group,
@ -432,7 +896,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
.unwrap()
.open_orders;
let mut order_placer = SerumOrderPlacer {
let order_placer = SerumOrderPlacer {
solana: solana.clone(),
serum: context.serum.clone(),
account,
@ -441,7 +905,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
open_orders,
next_client_order_id: 0,
};
let mut order_placer2 = SerumOrderPlacer {
let order_placer2 = SerumOrderPlacer {
solana: solana.clone(),
serum: context.serum.clone(),
account: account2,
@ -451,163 +915,12 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
next_client_order_id: 100000,
};
//
// TEST: Placing and canceling an order does not take loan origination fees even if borrows are needed
//
{
let (bid_order_id, _) = order_placer.bid(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(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, &[open_orders])
.await;
CommonSetup {
_group_with_tokens: group_with_tokens,
serum_market_cookie,
quote_bank,
base_bank,
order_placer,
order_placer2,
}
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::<Bank>(quote_bank)
.await
.collected_fees_native;
// account2 has an order on the book
order_placer2.bid(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::<Bank>(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, &[open_orders, open_orders2])
.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::<Bank>(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::<Bank>(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::<MangoAccount>(account).await;
assert_eq!(
account_data.buyback_fees_accrued_current,
serum_fee(fill_amount) as u64
);
assert_eq!(
account_position(solana, account, quote_bank).await,
deposit_amount + without_serum_taker_fee(fill_amount)
);
}
Ok(())
}

View File

@ -2417,6 +2417,92 @@ impl ClientInstruction for Serum3SettleFundsInstruction {
}
}
pub struct Serum3SettleFundsV2Instruction {
pub account: Pubkey,
pub owner: TestKeypair,
pub serum_market: Pubkey,
pub fees_to_dao: bool,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for Serum3SettleFundsV2Instruction {
type Accounts = mango_v4::accounts::Serum3SettleFundsV2;
type Instruction = mango_v4::instruction::Serum3SettleFundsV2;
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 {
fees_to_dao: self.fees_to_dao,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap();
let open_orders = account
.serum3_orders(serum_market.market_index)
.unwrap()
.open_orders;
let quote_info =
get_mint_info_by_token_index(&account_loader, &account, serum_market.quote_token_index)
.await;
let base_info =
get_mint_info_by_token_index(&account_loader, &account, serum_market.base_token_index)
.await;
let market_external_bytes = account_loader
.load_bytes(&serum_market.serum_market_external)
.await
.unwrap();
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&market_external_bytes[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
// unpack the data, to avoid unaligned references
let coin_vault = market_external.coin_vault;
let pc_vault = market_external.pc_vault;
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&serum_market.serum_market_external,
&serum_market.serum_program,
)
.unwrap();
let accounts = Self::Accounts {
v1: mango_v4::accounts::Serum3SettleFunds {
group: account.fixed.group,
account: self.account,
open_orders,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
serum_market: self.serum_market,
serum_program: serum_market.serum_program,
serum_market_external: serum_market.serum_market_external,
market_base_vault: from_serum_style_pubkey(&coin_vault),
market_quote_vault: from_serum_style_pubkey(&pc_vault),
market_vault_signer: vault_signer,
owner: self.owner.pubkey(),
token_program: Token::id(),
},
v2: mango_v4::accounts::Serum3SettleFundsV2Extra {
quote_oracle: quote_info.oracle,
base_oracle: base_info.oracle,
},
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct Serum3LiqForceCancelOrdersInstruction {
pub account: Pubkey,
pub serum_market: Pubkey,

View File

@ -77,6 +77,7 @@ export type MangoClientOptions = {
postSendTxCallback?: ({ txid }: { txid: string }) => void;
prioritizationFee?: number;
txConfirmationCommitment?: Commitment;
openbookFeesToDao?: boolean;
};
export class MangoClient {
@ -84,6 +85,7 @@ export class MangoClient {
private postSendTxCallback?: ({ txid }) => void;
private prioritizationFee: number;
private txConfirmationCommitment: Commitment;
private openbookFeesToDao: boolean;
constructor(
public program: Program<MangoV4>,
@ -94,6 +96,7 @@ export class MangoClient {
this.idsSource = opts?.idsSource || 'get-program-accounts';
this.prioritizationFee = opts?.prioritizationFee || 0;
this.postSendTxCallback = opts?.postSendTxCallback;
this.openbookFeesToDao = opts?.openbookFeesToDao ?? true;
this.txConfirmationCommitment =
opts?.txConfirmationCommitment ??
(program.provider as AnchorProvider).opts.commitment ??
@ -1638,6 +1641,12 @@ export class MangoClient {
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
): Promise<TransactionInstruction> {
if (this.openbookFeesToDao == false) {
throw new Error(
`openbookFeesToDao is set to false, please use serum3SettleFundsV2Ix`,
);
}
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
@ -1682,12 +1691,73 @@ export class MangoClient {
return ix;
}
public async serum3SettleFundsV2Ix(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
): Promise<TransactionInstruction> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const [serum3MarketExternalVaultSigner, openOrderPublicKey] =
await Promise.all([
generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
),
serum3Market.findOoPda(this.program.programId, mangoAccount.publicKey),
]);
const ix = await this.program.methods
.serum3SettleFundsV2(this.openbookFeesToDao)
.accounts({
v1: {
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: openOrderPublicKey,
serumMarket: serum3Market.publicKey,
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
serumMarketExternal: serum3Market.serumMarketExternal,
marketBaseVault: serum3MarketExternal.decoded.baseVault,
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
marketVaultSigner: serum3MarketExternalVaultSigner,
quoteBank: group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
).publicKey,
quoteVault: group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
).vault,
baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.publicKey,
baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.vault,
},
v2: {
quoteOracle: group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
).oracle,
baseOracle: group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
).oracle,
},
})
.instruction();
return ix;
}
public async serum3SettleFunds(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const ix = await this.serum3SettleFundsIx(
const ix = await this.serum3SettleFundsV2Ix(
group,
mangoAccount,
externalMarketPk,
@ -1745,7 +1815,7 @@ export class MangoClient {
side,
orderId,
),
this.serum3SettleFundsIx(group, mangoAccount, externalMarketPk),
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
]);
return await this.sendAndConfirmTransactionForGroup(group, ixes);
@ -3036,7 +3106,7 @@ export class MangoClient {
side,
orderId,
),
this.serum3SettleFundsIx(group, mangoAccount, externalMarketPk),
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
this.serum3PlaceOrderIx(
group,
mangoAccount,

View File

@ -2139,6 +2139,11 @@ export type MangoV4 = {
},
{
"name": "serum3SettleFunds",
"docs": [
"Settles all free funds from the OpenOrders account into the MangoAccount.",
"",
"Any serum \"referrer rebates\" (ui fees) are considered Mango fees."
],
"accounts": [
{
"name": "group",
@ -2221,6 +2226,119 @@ export type MangoV4 = {
],
"args": []
},
{
"name": "serum3SettleFundsV2",
"docs": [
"Like Serum3SettleFunds, but `fees_to_dao` determines if referrer rebates are considered fees",
"or are credited to the MangoAccount."
],
"accounts": [
{
"name": "v1",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "openOrders",
"isMut": true,
"isSigner": false
},
{
"name": "serumMarket",
"isMut": false,
"isSigner": false
},
{
"name": "serumProgram",
"isMut": false,
"isSigner": false
},
{
"name": "serumMarketExternal",
"isMut": true,
"isSigner": false
},
{
"name": "marketBaseVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketQuoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketVaultSigner",
"isMut": false,
"isSigner": false,
"docs": [
"needed for the automatic settle_funds call"
]
},
{
"name": "quoteBank",
"isMut": true,
"isSigner": false
},
{
"name": "quoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "baseBank",
"isMut": true,
"isSigner": false
},
{
"name": "baseVault",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
]
},
{
"name": "v2",
"accounts": [
{
"name": "quoteOracle",
"isMut": false,
"isSigner": false
},
{
"name": "baseOracle",
"isMut": false,
"isSigner": false
}
]
}
],
"args": [
{
"name": "feesToDao",
"type": "bool"
}
]
},
{
"name": "serum3LiqForceCancelOrders",
"accounts": [
@ -10634,6 +10752,11 @@ export const IDL: MangoV4 = {
},
{
"name": "serum3SettleFunds",
"docs": [
"Settles all free funds from the OpenOrders account into the MangoAccount.",
"",
"Any serum \"referrer rebates\" (ui fees) are considered Mango fees."
],
"accounts": [
{
"name": "group",
@ -10716,6 +10839,119 @@ export const IDL: MangoV4 = {
],
"args": []
},
{
"name": "serum3SettleFundsV2",
"docs": [
"Like Serum3SettleFunds, but `fees_to_dao` determines if referrer rebates are considered fees",
"or are credited to the MangoAccount."
],
"accounts": [
{
"name": "v1",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "openOrders",
"isMut": true,
"isSigner": false
},
{
"name": "serumMarket",
"isMut": false,
"isSigner": false
},
{
"name": "serumProgram",
"isMut": false,
"isSigner": false
},
{
"name": "serumMarketExternal",
"isMut": true,
"isSigner": false
},
{
"name": "marketBaseVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketQuoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketVaultSigner",
"isMut": false,
"isSigner": false,
"docs": [
"needed for the automatic settle_funds call"
]
},
{
"name": "quoteBank",
"isMut": true,
"isSigner": false
},
{
"name": "quoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "baseBank",
"isMut": true,
"isSigner": false
},
{
"name": "baseVault",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
]
},
{
"name": "v2",
"accounts": [
{
"name": "quoteOracle",
"isMut": false,
"isSigner": false
},
{
"name": "baseOracle",
"isMut": false,
"isSigner": false
}
]
}
],
"args": [
{
"name": "feesToDao",
"type": "bool"
}
]
},
{
"name": "serum3LiqForceCancelOrders",
"accounts": [