Serum: loan origination fee, don't auto-settle, fix vault check

- Loan origination fees: The previous approach of tracking the reserved
  amount did not work because OutEvents will also reduce the reserved
  amount. This means we can't know if it was an OutEvent-cancel or an
  order execution that caused the reduction.

  Instead, we now track the amount of borrows that was made (without
  applying origination fees) in place order. Whenever we try to settle
  and the amount of tokens on the oo account is less than the potential
  borrows, we can be certain that the borrow has actualized.

- Place order is no longer automatically followed by a settle.

  This can reduce compute use when people want to place multiple orders
  in sequence. Now they can use the HealthRegion instructions to place
  their orders, settle once at the end, and then have health checked.

- Vault check: Place order previously rejected valid orders because it
  didn't consider that there could be free tokens on the oo account.

- Tests: Some infrastructure for less verbose serum testing.
This commit is contained in:
Christian Kamm 2022-08-26 12:45:32 +02:00
parent e1adbf0217
commit dc4acd0dd7
7 changed files with 668 additions and 366 deletions

View File

@ -3,12 +3,9 @@ use anchor_lang::prelude::*;
use serum_dex::instruction::CancelOrderInstructionV2; use serum_dex::instruction::CancelOrderInstructionV2;
use crate::error::*; use crate::error::*;
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*; use crate::state::*;
use super::OpenOrdersSlim;
use super::Serum3Side; use super::Serum3Side;
use checked_math as cm;
#[derive(Accounts)] #[derive(Accounts)]
pub struct Serum3CancelOrder<'info> { pub struct Serum3CancelOrder<'info> {
@ -83,55 +80,15 @@ pub fn serum3_cancel_order(
// //
// Cancel // Cancel
// //
let before_oo = {
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
OpenOrdersSlim::from_oo(&open_orders)
};
let order = serum_dex::instruction::CancelOrderInstructionV2 { let order = serum_dex::instruction::CancelOrderInstructionV2 {
side: u8::try_from(side).unwrap().try_into().unwrap(), side: u8::try_from(side).unwrap().try_into().unwrap(),
order_id, order_id,
}; };
cpi_cancel_order(ctx.accounts, order)?; cpi_cancel_order(ctx.accounts, order)?;
{
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
let mut account = ctx.accounts.account.load_mut()?;
decrease_maybe_loan(
serum_market.market_index,
&mut account.borrow_mut(),
&before_oo,
&after_oo,
);
};
Ok(()) Ok(())
} }
// if free has increased, the free increase is reduction in reserved, reduce this from
// the cached
pub fn decrease_maybe_loan(
market_index: Serum3MarketIndex,
account: &mut MangoAccountRefMut,
before_oo: &OpenOrdersSlim,
after_oo: &OpenOrdersSlim,
) {
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
if after_oo.native_coin_free > before_oo.native_coin_free {
let native_coin_free_increase = after_oo.native_coin_free - before_oo.native_coin_free;
serum3_account.previous_native_coin_reserved =
cm!(serum3_account.previous_native_coin_reserved - native_coin_free_increase);
}
// pc
if after_oo.native_pc_free > before_oo.native_pc_free {
let free_pc_increase = after_oo.native_pc_free - before_oo.native_pc_free;
serum3_account.previous_native_pc_reserved =
cm!(serum3_account.previous_native_pc_reserved - free_pc_increase);
}
}
fn cpi_cancel_order(ctx: &Serum3CancelOrder, order: CancelOrderInstructionV2) -> Result<()> { fn cpi_cancel_order(ctx: &Serum3CancelOrder, order: CancelOrderInstructionV2) -> Result<()> {
use crate::serum3_cpi; use crate::serum3_cpi;
let group = ctx.group.load()?; let group = ctx.group.load()?;

View File

@ -2,11 +2,10 @@ use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount}; use anchor_spl::token::{Token, TokenAccount};
use crate::error::*; use crate::error::*;
use crate::instructions::apply_vault_difference; use crate::instructions::{apply_vault_difference, charge_loan_origination_fees, OpenOrdersSlim};
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*; use crate::state::*;
use crate::logs::LoanOriginationFeeInstruction;
#[derive(Accounts)] #[derive(Accounts)]
pub struct Serum3LiqForceCancelOrders<'info> { pub struct Serum3LiqForceCancelOrders<'info> {
pub group: AccountLoader<'info, Group>, pub group: AccountLoader<'info, Group>,
@ -116,6 +115,26 @@ pub fn serum3_liq_force_cancel_orders(
require!(health < 0, MangoError::SomeError); require!(health < 0, MangoError::SomeError);
} }
//
// Charge any open loan origination fees
//
{
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
let before_oo = OpenOrdersSlim::from_oo(&open_orders);
let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
serum_market.market_index,
&mut base_bank,
&mut quote_bank,
&mut account.borrow_mut(),
&before_oo,
)?;
}
// //
// Before-settle tracking // Before-settle tracking
// //
@ -136,12 +155,17 @@ pub fn serum3_liq_force_cancel_orders(
let after_base_vault = ctx.accounts.base_vault.amount; let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount; let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account // Settle cannot decrease vault balances
require_gte!(after_base_vault, before_base_vault);
require_gte!(after_quote_vault, before_quote_vault);
// Credit the difference in vault balances to the user's account
let mut account = ctx.accounts.account.load_mut()?; let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let difference_result = apply_vault_difference( apply_vault_difference(
&mut account.borrow_mut(), &mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank, &mut base_bank,
after_base_vault, after_base_vault,
before_base_vault, before_base_vault,
@ -149,11 +173,6 @@ pub fn serum3_liq_force_cancel_orders(
after_quote_vault, after_quote_vault,
before_quote_vault, before_quote_vault,
)?; )?;
difference_result.log_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders,
);
Ok(()) Ok(())
} }

View File

@ -12,15 +12,14 @@ use serum_dex::instruction::NewOrderInstructionV3;
use serum_dex::matching::Side; use serum_dex::matching::Side;
use serum_dex::state::OpenOrders; use serum_dex::state::OpenOrders;
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
/// For loan origination fees bookkeeping purposes /// For loan origination fees bookkeeping purposes
#[derive(Debug)]
pub struct OpenOrdersSlim { pub struct OpenOrdersSlim {
pub native_coin_free: u64, native_coin_free: u64,
pub native_coin_total: u64, native_coin_total: u64,
pub native_pc_free: u64, native_pc_free: u64,
pub native_pc_total: u64, native_pc_total: u64,
pub referrer_rebates_accrued: u64, referrer_rebates_accrued: u64,
} }
impl OpenOrdersSlim { impl OpenOrdersSlim {
pub fn from_oo(oo: &OpenOrders) -> Self { pub fn from_oo(oo: &OpenOrders) -> Self {
@ -38,7 +37,10 @@ pub trait OpenOrdersAmounts {
fn native_base_reserved(&self) -> u64; fn native_base_reserved(&self) -> u64;
fn native_quote_reserved(&self) -> u64; fn native_quote_reserved(&self) -> u64;
fn native_base_free(&self) -> u64; fn native_base_free(&self) -> u64;
fn native_quote_free(&self) -> u64; // includes settleable referrer rebates fn native_quote_free(&self) -> u64;
fn native_quote_free_plus_rebates(&self) -> u64;
fn native_base_total(&self) -> u64;
fn native_quote_total_plus_rebates(&self) -> u64;
} }
impl OpenOrdersAmounts for OpenOrdersSlim { impl OpenOrdersAmounts for OpenOrdersSlim {
@ -52,8 +54,17 @@ impl OpenOrdersAmounts for OpenOrdersSlim {
self.native_coin_free self.native_coin_free
} }
fn native_quote_free(&self) -> u64 { fn native_quote_free(&self) -> u64 {
self.native_pc_free
}
fn native_quote_free_plus_rebates(&self) -> u64 {
cm!(self.native_pc_free + self.referrer_rebates_accrued) cm!(self.native_pc_free + self.referrer_rebates_accrued)
} }
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
} }
impl OpenOrdersAmounts for OpenOrders { impl OpenOrdersAmounts for OpenOrders {
@ -67,8 +78,17 @@ impl OpenOrdersAmounts for OpenOrders {
self.native_coin_free self.native_coin_free
} }
fn native_quote_free(&self) -> u64 { fn native_quote_free(&self) -> u64 {
self.native_pc_free
}
fn native_quote_free_plus_rebates(&self) -> u64 {
cm!(self.native_pc_free + self.referrer_rebates_accrued) cm!(self.native_pc_free + self.referrer_rebates_accrued)
} }
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
} }
/// Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them /// Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them
@ -222,27 +242,6 @@ pub fn serum3_place_order(
); );
} }
//
// Before-order tracking
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
// Provide a readable error message in case the vault doesn't have enough tokens
let (vault_amount, needed_amount) = match side {
Serum3Side::Ask => (before_base_vault, max_base_qty),
Serum3Side::Bid => (before_quote_vault, max_native_quote_qty_including_fees),
};
if vault_amount < needed_amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
needed_amount, vault_amount
)
});
}
// //
// Pre-health computation // Pre-health computation
// //
@ -259,8 +258,40 @@ pub fn serum3_place_order(
}; };
// //
// Apply the order to serum. Also immediately settle, in case the order // Before-order tracking
// matched against an existing other order. //
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
let before_oo = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
OpenOrdersSlim::from_oo(&open_orders)
};
// Provide a readable error message in case the vault doesn't have enough tokens
let (vault_amount, needed_amount) = match side {
Serum3Side::Ask => (
before_base_vault,
max_base_qty.saturating_sub(before_oo.native_base_free()),
),
Serum3Side::Bid => (
before_quote_vault,
max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()),
),
};
if vault_amount < needed_amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
needed_amount, vault_amount
)
});
}
//
// Apply the order to serum
// //
let order = serum_dex::instruction::NewOrderInstructionV3 { let order = serum_dex::instruction::NewOrderInstructionV3 {
side: u8::try_from(side).unwrap().try_into().unwrap(), side: u8::try_from(side).unwrap().try_into().unwrap(),
@ -275,26 +306,12 @@ pub fn serum3_place_order(
client_order_id, client_order_id,
limit, limit,
}; };
let before_oo = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
OpenOrdersSlim::from_oo(&open_orders)
};
cpi_place_order(ctx.accounts, order)?; cpi_place_order(ctx.accounts, order)?;
cpi_settle_funds(ctx.accounts)?;
let oo_difference = { let oo_difference = {
let oo_ai = &ctx.accounts.open_orders.as_ref(); let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?; let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders); let after_oo = OpenOrdersSlim::from_oo(&open_orders);
inc_maybe_loan(
serum_market.market_index,
&mut account.borrow_mut(),
&before_oo,
&after_oo,
);
OODifference::new(&before_oo, &after_oo) OODifference::new(&before_oo, &after_oo)
}; };
@ -306,6 +323,10 @@ pub fn serum3_place_order(
let after_base_vault = ctx.accounts.base_vault.amount; let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount; let after_quote_vault = ctx.accounts.quote_vault.amount;
// Placing an order cannot increase vault balances
require_gte!(before_base_vault, after_base_vault);
require_gte!(before_quote_vault, after_quote_vault);
// Charge the difference in vault balances to the user's account // Charge the difference in vault balances to the user's account
let vault_difference = { let vault_difference = {
let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?;
@ -313,6 +334,7 @@ pub fn serum3_place_order(
apply_vault_difference( apply_vault_difference(
&mut account.borrow_mut(), &mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank, &mut base_bank,
after_base_vault, after_base_vault,
before_base_vault, before_base_vault,
@ -331,39 +353,9 @@ pub fn serum3_place_order(
account.check_health_post(&health_cache, pre_health)?; account.check_health_post(&health_cache, pre_health)?;
} }
vault_difference.log_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
LoanOriginationFeeInstruction::Serum3PlaceOrder,
);
Ok(()) Ok(())
} }
// if reserved has increased, then increase cached value by the increase in reserved
pub fn inc_maybe_loan(
market_index: Serum3MarketIndex,
account: &mut MangoAccountRefMut,
before_oo: &OpenOrdersSlim,
after_oo: &OpenOrdersSlim,
) {
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
if after_oo.native_base_reserved() > before_oo.native_base_reserved() {
let native_coin_reserved_increase =
after_oo.native_base_reserved() - before_oo.native_base_reserved();
serum3_account.previous_native_coin_reserved =
cm!(serum3_account.previous_native_coin_reserved + native_coin_reserved_increase);
}
if after_oo.native_quote_reserved() > before_oo.native_quote_reserved() {
let reserved_pc_increase =
after_oo.native_quote_reserved() - before_oo.native_quote_reserved();
serum3_account.previous_native_pc_reserved =
cm!(serum3_account.previous_native_pc_reserved + reserved_pc_increase);
}
}
pub struct OODifference { pub struct OODifference {
reserved_base_change: I80F48, reserved_base_change: I80F48,
reserved_quote_change: I80F48, reserved_quote_change: I80F48,
@ -380,8 +372,8 @@ impl OODifference {
- I80F48::from(before_oo.native_quote_reserved())), - I80F48::from(before_oo.native_quote_reserved())),
free_base_change: cm!(I80F48::from(after_oo.native_base_free()) free_base_change: cm!(I80F48::from(after_oo.native_base_free())
- I80F48::from(before_oo.native_base_free())), - I80F48::from(before_oo.native_base_free())),
free_quote_change: cm!(I80F48::from(after_oo.native_quote_free()) free_quote_change: cm!(I80F48::from(after_oo.native_quote_free_plus_rebates())
- I80F48::from(before_oo.native_quote_free())), - I80F48::from(before_oo.native_quote_free_plus_rebates())),
} }
} }
@ -402,55 +394,14 @@ impl OODifference {
} }
} }
pub struct VaultDifferenceResult { pub struct VaultDifference {
base_raw_index: usize,
base_index: TokenIndex, base_index: TokenIndex,
base_active: bool,
quote_raw_index: usize,
quote_index: TokenIndex, quote_index: TokenIndex,
quote_active: bool,
base_loan_origination_fee: I80F48,
quote_loan_origination_fee: I80F48,
base_native_change: I80F48, base_native_change: I80F48,
quote_native_change: I80F48, quote_native_change: I80F48,
} }
impl VaultDifferenceResult { impl VaultDifference {
pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccountRefMut) {
if !self.base_active {
account.deactivate_token_position(self.base_raw_index);
}
if !self.quote_active {
account.deactivate_token_position(self.quote_raw_index);
}
}
pub fn log_loan_origination_fees(
&self,
group: &Pubkey,
account: &Pubkey,
instruction: LoanOriginationFeeInstruction,
) {
if self.base_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: *group,
mango_account: *account,
token_index: self.base_index,
loan_origination_fee: self.base_loan_origination_fee.to_bits(),
instruction,
});
}
if self.quote_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: *group,
mango_account: *account,
token_index: self.quote_index,
loan_origination_fee: self.quote_loan_origination_fee.to_bits(),
instruction,
});
}
}
pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> { pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> {
health_cache.adjust_token_balance(self.base_index, self.base_native_change)?; health_cache.adjust_token_balance(self.base_index, self.base_native_change)?;
health_cache.adjust_token_balance(self.quote_index, self.quote_native_change)?; health_cache.adjust_token_balance(self.quote_index, self.quote_native_change)?;
@ -458,45 +409,68 @@ impl VaultDifferenceResult {
} }
} }
/// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after
/// changing the vault balances
pub fn apply_vault_difference( pub fn apply_vault_difference(
account: &mut MangoAccountRefMut, account: &mut MangoAccountRefMut,
serum_market_index: Serum3MarketIndex,
base_bank: &mut Bank, base_bank: &mut Bank,
after_base_vault: u64, after_base_vault: u64,
before_base_vault: u64, before_base_vault: u64,
quote_bank: &mut Bank, quote_bank: &mut Bank,
after_quote_vault: u64, after_quote_vault: u64,
before_quote_vault: u64, before_quote_vault: u64,
) -> Result<VaultDifferenceResult> { ) -> Result<VaultDifference> {
// TODO: Applying the loan origination fee here may be too early: it should only be
// charged if an order executes and the loan materializes? Otherwise MMs that place
// an order without having the funds will be charged for each place_order!
let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?;
let base_native_before = base_position.native(&base_bank);
let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault)); let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault));
let (base_active, base_loan_origination_fee) =
base_bank.change_with_fee(base_position, base_needed_change)?;
let base_native_after = base_position.native(&base_bank);
let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?; let (base_position, _) = account.token_position_mut(base_bank.token_index)?;
let quote_native_before = quote_position.native(&quote_bank); let base_native_before = base_position.native(&base_bank);
base_bank.change_without_fee(base_position, base_needed_change)?;
let base_native_after = base_position.native(&base_bank);
let base_native_change = cm!(base_native_after - base_native_before);
let base_borrows = base_native_change
.max(base_native_after)
.min(I80F48::ZERO)
.abs()
.to_num::<u64>();
let quote_needed_change = let quote_needed_change =
cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault)); cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault));
let (quote_active, quote_loan_origination_fee) =
quote_bank.change_with_fee(quote_position, quote_needed_change)?;
let quote_native_after = quote_position.native(&quote_bank);
Ok(VaultDifferenceResult { let (quote_position, _) = account.token_position_mut(quote_bank.token_index)?;
base_raw_index, let quote_native_before = quote_position.native(&quote_bank);
quote_bank.change_without_fee(quote_position, quote_needed_change)?;
let quote_native_after = quote_position.native(&quote_bank);
let quote_native_change = cm!(quote_native_after - quote_native_before);
let quote_borrows = quote_native_change
.max(quote_native_after)
.min(I80F48::ZERO)
.abs()
.to_num::<u64>();
let market = account.serum3_orders_mut(serum_market_index).unwrap();
// Only for place: Add to potential borrow amounts
market.base_borrows_without_fee = cm!(market.base_borrows_without_fee + base_borrows);
market.quote_borrows_without_fee = cm!(market.quote_borrows_without_fee + quote_borrows);
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
if base_needed_change > 0 {
market.base_borrows_without_fee = market
.base_borrows_without_fee
.saturating_sub(base_needed_change.to_num::<u64>());
}
if quote_needed_change > 0 {
market.quote_borrows_without_fee = market
.quote_borrows_without_fee
.saturating_sub(quote_needed_change.to_num::<u64>());
}
Ok(VaultDifference {
base_index: base_bank.token_index, base_index: base_bank.token_index,
base_active,
quote_raw_index,
quote_index: quote_bank.token_index, quote_index: quote_bank.token_index,
quote_active, base_native_change,
base_loan_origination_fee, quote_native_change,
quote_loan_origination_fee,
base_native_change: cm!(base_native_after - base_native_before),
quote_native_change: cm!(quote_native_after - quote_native_before),
}) })
} }
@ -526,21 +500,3 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu
} }
.call(&group, order) .call(&group, order)
} }
fn cpi_settle_funds(ctx: &Serum3PlaceOrder) -> Result<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;
serum3_cpi::SettleFunds {
program: ctx.serum_program.to_account_info(),
market: ctx.serum_market_external.to_account_info(),
open_orders: ctx.open_orders.to_account_info(),
open_orders_authority: ctx.group.to_account_info(),
base_vault: ctx.market_base_vault.to_account_info(),
quote_vault: ctx.market_quote_vault.to_account_info(),
user_base_wallet: ctx.base_vault.to_account_info(),
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(),
}
.call(&group)
}

View File

@ -1,5 +1,3 @@
use std::borrow::BorrowMut;
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount}; use anchor_spl::token::{Token, TokenAccount};
@ -10,7 +8,7 @@ use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*; use crate::state::*;
use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim}; use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim};
use crate::logs::LoanOriginationFeeInstruction; use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
#[derive(Accounts)] #[derive(Accounts)]
pub struct Serum3SettleFunds<'info> { pub struct Serum3SettleFunds<'info> {
@ -116,34 +114,35 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
} }
// //
// Before-order tracking // Charge any open loan origination fees
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
//
// Settle
// //
{ {
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
cpi_settle_funds(ctx.accounts)?; let before_oo = OpenOrdersSlim::from_oo(&open_orders);
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
let mut account = ctx.accounts.account.load_mut()?; let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
charge_maybe_fees( charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
serum_market.market_index, serum_market.market_index,
&mut base_bank, &mut base_bank,
&mut quote_bank, &mut quote_bank,
&mut account.borrow_mut(), &mut account.borrow_mut(),
&after_oo, &before_oo,
)?; )?;
} }
// //
// After-order tracking // Settle
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
cpi_settle_funds(ctx.accounts)?;
//
// After-settle tracking
// //
{ {
ctx.accounts.base_vault.reload()?; ctx.accounts.base_vault.reload()?;
@ -151,12 +150,17 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
let after_base_vault = ctx.accounts.base_vault.amount; let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount; let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account // Settle cannot decrease vault balances
require_gte!(after_base_vault, before_base_vault);
require_gte!(after_quote_vault, before_quote_vault);
// Credit the difference in vault balances to the user's account
let mut account = ctx.accounts.account.load_mut()?; let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let difference_result = apply_vault_difference( apply_vault_difference(
&mut account.borrow_mut(), &mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank, &mut base_bank,
after_base_vault, after_base_vault,
before_base_vault, before_base_vault,
@ -164,73 +168,70 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
after_quote_vault, after_quote_vault,
before_quote_vault, before_quote_vault,
)?; )?;
difference_result.log_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
LoanOriginationFeeInstruction::Serum3SettleFunds,
);
} }
Ok(()) Ok(())
} }
// if reserved is less than cached, charge loan fee on the difference // Charge fees if the potential borrows are bigger than the funds on the open orders account
pub fn charge_maybe_fees( pub fn charge_loan_origination_fees(
group_pubkey: &Pubkey,
account_pubkey: &Pubkey,
market_index: Serum3MarketIndex, market_index: Serum3MarketIndex,
coin_bank: &mut Bank, base_bank: &mut Bank,
pc_bank: &mut Bank, quote_bank: &mut Bank,
account: &mut MangoAccountRefMut, account: &mut MangoAccountRefMut,
after_oo: &OpenOrdersSlim, before_oo: &OpenOrdersSlim,
) -> Result<()> { ) -> Result<()> {
let serum3_account = account.serum3_orders_mut(market_index).unwrap(); let serum3_account = account.serum3_orders_mut(market_index).unwrap();
let maybe_actualized_coin_loan = I80F48::from_num::<u64>( let oo_base_total = before_oo.native_base_total();
let actualized_base_loan = I80F48::from_num(
serum3_account serum3_account
.previous_native_coin_reserved .base_borrows_without_fee
.saturating_sub(after_oo.native_base_reserved()), .saturating_sub(oo_base_total),
); );
if actualized_base_loan > 0 {
serum3_account.base_borrows_without_fee = oo_base_total;
if maybe_actualized_coin_loan > 0 { // now that the loan is actually materialized, charge the loan origination fee
serum3_account.previous_native_coin_reserved = after_oo.native_base_reserved();
// loan origination fees
let coin_token_account = account.token_position_mut(coin_bank.token_index)?.0;
let coin_token_native = coin_token_account.native(coin_bank);
if coin_token_native.is_negative() {
let actualized_loan = coin_token_native.abs().min(maybe_actualized_coin_loan);
// note: the withdraw has already happened while placing the order // note: the withdraw has already happened while placing the order
// now that the loan is actually materialized (since the fill having taken place) let base_token_account = account.token_position_mut(base_bank.token_index)?.0;
// charge the loan origination fee let (_, fee) =
coin_bank base_bank.withdraw_loan_origination_fee(base_token_account, actualized_base_loan)?;
.borrow_mut()
.withdraw_loan_origination_fee(coin_token_account, actualized_loan)?; emit!(WithdrawLoanOriginationFeeLog {
} mango_group: *group_pubkey,
mango_account: *account_pubkey,
token_index: base_bank.token_index,
loan_origination_fee: fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
});
} }
let serum3_account = account.serum3_orders_mut(market_index).unwrap(); let serum3_account = account.serum3_orders_mut(market_index).unwrap();
let maybe_actualized_pc_loan = I80F48::from_num::<u64>( let oo_quote_total = before_oo.native_quote_total_plus_rebates();
let actualized_quote_loan = I80F48::from_num::<u64>(
serum3_account serum3_account
.previous_native_pc_reserved .quote_borrows_without_fee
.saturating_sub(after_oo.native_quote_reserved()), .saturating_sub(oo_quote_total),
); );
if actualized_quote_loan > 0 {
serum3_account.quote_borrows_without_fee = oo_quote_total;
if maybe_actualized_pc_loan > 0 { // now that the loan is actually materialized, charge the loan origination fee
serum3_account.previous_native_pc_reserved = after_oo.native_quote_reserved();
// loan origination fees
let pc_token_account = account.token_position_mut(pc_bank.token_index)?.0;
let pc_token_native = pc_token_account.native(pc_bank);
if pc_token_native.is_negative() {
let actualized_loan = pc_token_native.abs().min(maybe_actualized_pc_loan);
// note: the withdraw has already happened while placing the order // note: the withdraw has already happened while placing the order
// now that the loan is actually materialized (since the fill having taken place) let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0;
// charge the loan origination fee let (_, fee) =
pc_bank quote_bank.withdraw_loan_origination_fee(quote_token_account, actualized_quote_loan)?;
.borrow_mut()
.withdraw_loan_origination_fee(pc_token_account, actualized_loan)?; emit!(WithdrawLoanOriginationFeeLog {
} mango_group: *group_pubkey,
mango_account: *account_pubkey,
token_index: quote_bank.token_index,
loan_origination_fee: fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
});
} }
Ok(()) Ok(())

View File

@ -92,12 +92,12 @@ impl TokenPosition {
pub struct Serum3Orders { pub struct Serum3Orders {
pub open_orders: Pubkey, pub open_orders: Pubkey,
// tracks reserved funds in open orders account, /// Tracks the amount of borrows that have flowed into the serum open orders account.
// used for bookkeeping of potentital loans which /// These borrows did not have the loan origination fee applied, and that may happen
// can be charged with loan origination fees /// later (in serum3_settle_funds) if we can guarantee that the funds were used.
// e.g. serum3 settle funds ix /// In particular a place-on-book, cancel, settle should not cost fees.
pub previous_native_coin_reserved: u64, pub base_borrows_without_fee: u64,
pub previous_native_pc_reserved: u64, pub quote_borrows_without_fee: u64,
pub market_index: Serum3MarketIndex, pub market_index: Serum3MarketIndex,
@ -138,8 +138,8 @@ impl Default for Serum3Orders {
quote_token_index: TokenIndex::MAX, quote_token_index: TokenIndex::MAX,
reserved: [0; 64], reserved: [0; 64],
padding: Default::default(), padding: Default::default(),
previous_native_coin_reserved: 0, base_borrows_without_fee: 0,
previous_native_pc_reserved: 0, quote_borrows_without_fee: 0,
} }
} }
} }

View File

@ -190,16 +190,16 @@ impl SerumCookie {
pub async fn consume_spot_events( pub async fn consume_spot_events(
&self, &self,
spot_market_cookie: &SpotMarketCookie, spot_market_cookie: &SpotMarketCookie,
open_orders: Pubkey, open_orders: &[Pubkey],
) { ) {
let instructions = [serum_dex::instruction::consume_events( let instructions = [serum_dex::instruction::consume_events(
&self.program_id, &self.program_id,
vec![&open_orders], open_orders.iter().collect(),
&spot_market_cookie.market, &spot_market_cookie.market,
&spot_market_cookie.event_q, &spot_market_cookie.event_q,
&spot_market_cookie.coin_fee_account, &spot_market_cookie.coin_fee_account,
&spot_market_cookie.pc_fee_account, &spot_market_cookie.pc_fee_account,
5, 10,
) )
.unwrap()]; .unwrap()];
self.solana self.solana

View File

@ -1,17 +1,153 @@
#![cfg(feature = "test-bpf")] #![cfg(feature = "test-bpf")]
use solana_program_test::*; use solana_program_test::*;
use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError}; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::instructions::{
OpenOrdersSlim, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
};
use mango_v4::state::Serum3Orders;
use program_test::*; use program_test::*;
mod program_test; mod program_test;
use mango_setup::*; use mango_setup::*;
use std::sync::Arc;
struct SerumOrderPlacer {
solana: Arc<SolanaCookie>,
serum: Arc<SerumCookie>,
account: Pubkey,
owner: Keypair,
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 bid(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
let client_order_id = self.inc_client_order_id();
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)
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
.unwrap();
self.find_order_id_for_client_order_id(client_order_id)
.await
}
async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
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
.unwrap();
self.find_order_id_for_client_order_id(client_order_id)
.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 settle(&self) {
// to avoid multiple settles looking like the same tx
self.solana.advance_by_slots(1).await;
send_tx(
&self.solana,
Serum3SettleFundsInstruction {
account: self.account,
owner: &self.owner,
serum_market: self.serum_market,
},
)
.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 {
OpenOrdersSlim::from_oo(&self.serum.load_open_orders(self.open_orders).await)
}
}
#[tokio::test] #[tokio::test]
async fn test_serum() -> Result<(), TransportError> { async fn test_serum_basics() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new(); let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
let context = test_builder.start_default().await; let context = test_builder.start_default().await;
@ -36,19 +172,6 @@ async fn test_serum() -> Result<(), TransportError> {
let base_token = &tokens[0]; let base_token = &tokens[0];
let quote_token = &tokens[1]; let quote_token = &tokens[1];
let deposit_amount = 1000;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
// //
// SETUP: Create serum market // SETUP: Create serum market
// //
@ -77,6 +200,22 @@ async fn test_serum() -> Result<(), TransportError> {
.unwrap() .unwrap()
.serum_market; .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 // TEST: Create an open orders account
// //
@ -102,28 +241,20 @@ async fn test_serum() -> Result<(), TransportError> {
[(open_orders, 0)] [(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 // TEST: Place an order
// //
send_tx( let (order_id, _) = order_placer.bid(1.0, 100).await.unwrap();
solana,
Serum3PlaceOrderInstruction {
side: Serum3Side::Bid,
limit_price: 10, // in quote_lot (10) per base lot (100)
max_base_qty: 1, // in base lot (100)
max_native_quote_qty_including_fees: 100,
self_trade_behavior: Serum3SelfTradeBehavior::DecrementTake,
order_type: Serum3OrderType::Limit,
client_order_id: 0,
limit: 10,
account,
owner,
serum_market,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account).await; check_prev_instruction_post_health(&solana, account).await;
let native0 = account_position(solana, account, base_token.bank).await; let native0 = account_position(solana, account, base_token.bank).await;
@ -131,62 +262,46 @@ async fn test_serum() -> Result<(), TransportError> {
assert_eq!(native0, 1000); assert_eq!(native0, 1000);
assert_eq!(native1, 900); assert_eq!(native1, 900);
// get the order id let account_data = get_mango_account(solana, account).await;
let open_orders_bytes = solana.get_account_data(open_orders).await.unwrap(); let serum_orders = account_data.serum3_orders_by_raw_index(0);
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes( assert_eq!(serum_orders.base_borrows_without_fee, 0);
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()], assert_eq!(serum_orders.quote_borrows_without_fee, 0);
);
let order_id = open_orders_data.orders[0];
assert!(order_id != 0); assert!(order_id != 0);
// //
// TEST: Cancel the order // TEST: Cancel the order
// //
send_tx( order_placer.cancel(order_id).await;
solana,
Serum3CancelOrderInstruction {
side: Serum3Side::Bid,
order_id,
account,
owner,
serum_market,
},
)
.await
.unwrap();
// //
// TEST: Settle, moving the freed up funds back // TEST: Settle, moving the freed up funds back
// //
send_tx( order_placer.settle().await;
solana,
Serum3SettleFundsInstruction {
account,
owner,
serum_market,
},
)
.await
.unwrap();
let native0 = account_position(solana, account, base_token.bank).await; let native0 = account_position(solana, account, base_token.bank).await;
let native1 = account_position(solana, account, quote_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await;
assert_eq!(native0, 1000); assert_eq!(native0, 1000);
assert_eq!(native1, 1000); assert_eq!(native1, 1000);
// 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 // close oo account
// TODO: custom program error: 0x2a TooManyOpenOrders https://github.com/project-serum/serum-dex/blob/master/dex/src/error.rs#L88 send_tx(
// send_tx( solana,
// solana, Serum3CloseOpenOrdersInstruction {
// Serum3CloseOpenOrdersInstruction { account,
// account, serum_market,
// serum_market, owner,
// owner, sol_destination: payer.pubkey(),
// sol_destination: payer.pubkey(), },
// }, )
// ) .await
// .await .unwrap();
// .unwrap();
// deregister serum3 market // deregister serum3 market
send_tx( send_tx(
@ -203,3 +318,257 @@ async fn test_serum() -> Result<(), TransportError> {
Ok(()) 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(95_000); // Serum3PlaceOrder needs 92.8k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = &Keypair::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 GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let base_token = &tokens[0];
let base_bank = base_token.bank;
let quote_token = &tokens[1];
let quote_bank = quote_token.bank;
//
// SETUP: Create serum market
//
let serum_market_cookie = context
.serum
.list_spot_market(&base_token.mint, &quote_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 deposit_amount = 180000;
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 mut order_placer = SerumOrderPlacer {
solana: solana.clone(),
serum: context.serum.clone(),
account,
owner: owner.clone(),
serum_market,
open_orders,
next_client_order_id: 0,
};
let mut 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,
};
//
// 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;
}
let without_serum_taker_fee = |amount: i64| (amount as f64 * (1.0 - 0.0022)).trunc() as i64;
let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0003).round() 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;
// 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)
);
// 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)
);
}
Ok(())
}