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 crate::error::*;
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
use super::OpenOrdersSlim;
use super::Serum3Side;
use checked_math as cm;
#[derive(Accounts)]
pub struct Serum3CancelOrder<'info> {
@ -83,55 +80,15 @@ pub fn serum3_cancel_order(
//
// 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 {
side: u8::try_from(side).unwrap().try_into().unwrap(),
order_id,
};
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(())
}
// 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<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;

View File

@ -2,11 +2,10 @@ use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
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::logs::LoanOriginationFeeInstruction;
#[derive(Accounts)]
pub struct Serum3LiqForceCancelOrders<'info> {
pub group: AccountLoader<'info, Group>,
@ -116,6 +115,26 @@ pub fn serum3_liq_force_cancel_orders(
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
//
@ -136,12 +155,17 @@ pub fn serum3_liq_force_cancel_orders(
let after_base_vault = ctx.accounts.base_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 base_bank = ctx.accounts.base_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(),
serum_market.market_index,
&mut base_bank,
after_base_vault,
before_base_vault,
@ -149,11 +173,6 @@ pub fn serum3_liq_force_cancel_orders(
after_quote_vault,
before_quote_vault,
)?;
difference_result.log_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders,
);
Ok(())
}

View File

@ -12,15 +12,14 @@ use serum_dex::instruction::NewOrderInstructionV3;
use serum_dex::matching::Side;
use serum_dex::state::OpenOrders;
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
/// For loan origination fees bookkeeping purposes
#[derive(Debug)]
pub struct OpenOrdersSlim {
pub native_coin_free: u64,
pub native_coin_total: u64,
pub native_pc_free: u64,
pub native_pc_total: u64,
pub referrer_rebates_accrued: u64,
native_coin_free: u64,
native_coin_total: u64,
native_pc_free: u64,
native_pc_total: u64,
referrer_rebates_accrued: u64,
}
impl OpenOrdersSlim {
pub fn from_oo(oo: &OpenOrders) -> Self {
@ -38,7 +37,10 @@ pub trait OpenOrdersAmounts {
fn native_base_reserved(&self) -> u64;
fn native_quote_reserved(&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 {
@ -52,8 +54,17 @@ impl OpenOrdersAmounts for OpenOrdersSlim {
self.native_coin_free
}
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)
}
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 {
@ -67,8 +78,17 @@ impl OpenOrdersAmounts for OpenOrders {
self.native_coin_free
}
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)
}
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
@ -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
//
@ -259,8 +258,40 @@ pub fn serum3_place_order(
};
//
// Apply the order to serum. Also immediately settle, in case the order
// matched against an existing other order.
// Before-order tracking
//
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 {
side: u8::try_from(side).unwrap().try_into().unwrap(),
@ -275,26 +306,12 @@ pub fn serum3_place_order(
client_order_id,
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_settle_funds(ctx.accounts)?;
let oo_difference = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
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)
};
@ -306,6 +323,10 @@ pub fn serum3_place_order(
let after_base_vault = ctx.accounts.base_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
let vault_difference = {
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
@ -313,6 +334,7 @@ pub fn serum3_place_order(
apply_vault_difference(
&mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank,
after_base_vault,
before_base_vault,
@ -331,39 +353,9 @@ pub fn serum3_place_order(
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(())
}
// 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 {
reserved_base_change: I80F48,
reserved_quote_change: I80F48,
@ -380,8 +372,8 @@ impl OODifference {
- I80F48::from(before_oo.native_quote_reserved())),
free_base_change: cm!(I80F48::from(after_oo.native_base_free())
- I80F48::from(before_oo.native_base_free())),
free_quote_change: cm!(I80F48::from(after_oo.native_quote_free())
- I80F48::from(before_oo.native_quote_free())),
free_quote_change: cm!(I80F48::from(after_oo.native_quote_free_plus_rebates())
- I80F48::from(before_oo.native_quote_free_plus_rebates())),
}
}
@ -402,55 +394,14 @@ impl OODifference {
}
}
pub struct VaultDifferenceResult {
base_raw_index: usize,
pub struct VaultDifference {
base_index: TokenIndex,
base_active: bool,
quote_raw_index: usize,
quote_index: TokenIndex,
quote_active: bool,
base_loan_origination_fee: I80F48,
quote_loan_origination_fee: I80F48,
base_native_change: I80F48,
quote_native_change: I80F48,
}
impl VaultDifferenceResult {
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,
});
}
}
impl VaultDifference {
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.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(
account: &mut MangoAccountRefMut,
serum_market_index: Serum3MarketIndex,
base_bank: &mut Bank,
after_base_vault: u64,
before_base_vault: u64,
quote_bank: &mut Bank,
after_quote_vault: u64,
before_quote_vault: u64,
) -> Result<VaultDifferenceResult> {
// 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);
) -> Result<VaultDifference> {
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 quote_native_before = quote_position.native(&quote_bank);
let (base_position, _) = account.token_position_mut(base_bank.token_index)?;
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 =
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 {
base_raw_index,
let (quote_position, _) = account.token_position_mut(quote_bank.token_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_active,
quote_raw_index,
quote_index: quote_bank.token_index,
quote_active,
base_loan_origination_fee,
quote_loan_origination_fee,
base_native_change: cm!(base_native_after - base_native_before),
quote_native_change: cm!(quote_native_after - quote_native_before),
base_native_change,
quote_native_change,
})
}
@ -526,21 +500,3 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu
}
.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_spl::token::{Token, TokenAccount};
@ -10,7 +8,7 @@ use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim};
use crate::logs::LoanOriginationFeeInstruction;
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
#[derive(Accounts)]
pub struct Serum3SettleFunds<'info> {
@ -116,34 +114,35 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
}
//
// Before-order tracking
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
//
// Settle
// Charge any open loan origination fees
//
{
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
cpi_settle_funds(ctx.accounts)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
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_maybe_fees(
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(),
&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()?;
@ -151,12 +150,17 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
let after_base_vault = ctx.accounts.base_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 base_bank = ctx.accounts.base_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(),
serum_market.market_index,
&mut base_bank,
after_base_vault,
before_base_vault,
@ -164,73 +168,70 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
after_quote_vault,
before_quote_vault,
)?;
difference_result.log_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
LoanOriginationFeeInstruction::Serum3SettleFunds,
);
}
Ok(())
}
// if reserved is less than cached, charge loan fee on the difference
pub fn charge_maybe_fees(
// Charge fees if the potential borrows are bigger than the funds on the open orders account
pub fn charge_loan_origination_fees(
group_pubkey: &Pubkey,
account_pubkey: &Pubkey,
market_index: Serum3MarketIndex,
coin_bank: &mut Bank,
pc_bank: &mut Bank,
base_bank: &mut Bank,
quote_bank: &mut Bank,
account: &mut MangoAccountRefMut,
after_oo: &OpenOrdersSlim,
before_oo: &OpenOrdersSlim,
) -> Result<()> {
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
.previous_native_coin_reserved
.saturating_sub(after_oo.native_base_reserved()),
.base_borrows_without_fee
.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 {
serum3_account.previous_native_coin_reserved = after_oo.native_base_reserved();
// now that the loan is actually materialized, charge the loan origination fee
// note: the withdraw has already happened while placing the order
let base_token_account = account.token_position_mut(base_bank.token_index)?.0;
let (_, fee) =
base_bank.withdraw_loan_origination_fee(base_token_account, actualized_base_loan)?;
// 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
// now that the loan is actually materialized (since the fill having taken place)
// charge the loan origination fee
coin_bank
.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 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
.previous_native_pc_reserved
.saturating_sub(after_oo.native_quote_reserved()),
.quote_borrows_without_fee
.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 {
serum3_account.previous_native_pc_reserved = after_oo.native_quote_reserved();
// now that the loan is actually materialized, charge the loan origination fee
// note: the withdraw has already happened while placing the order
let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0;
let (_, fee) =
quote_bank.withdraw_loan_origination_fee(quote_token_account, actualized_quote_loan)?;
// 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
// now that the loan is actually materialized (since the fill having taken place)
// charge the loan origination fee
pc_bank
.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(())

View File

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

View File

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

View File

@ -1,17 +1,153 @@
#![cfg(feature = "test-bpf")]
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::*;
mod program_test;
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]
async fn test_serum() -> Result<(), TransportError> {
async fn test_serum_basics() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
let context = test_builder.start_default().await;
@ -36,19 +172,6 @@ async fn test_serum() -> Result<(), TransportError> {
let base_token = &tokens[0];
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
//
@ -77,6 +200,22 @@ async fn test_serum() -> Result<(), TransportError> {
.unwrap()
.serum_market;
//
// SETUP: Create account
//
let deposit_amount = 1000;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: Create an open orders account
//
@ -102,28 +241,20 @@ async fn test_serum() -> Result<(), TransportError> {
[(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
//
send_tx(
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();
let (order_id, _) = order_placer.bid(1.0, 100).await.unwrap();
check_prev_instruction_post_health(&solana, account).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!(native1, 900);
// get the order id
let open_orders_bytes = solana.get_account_data(open_orders).await.unwrap();
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes(
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()],
);
let order_id = open_orders_data.orders[0];
let account_data = get_mango_account(solana, account).await;
let serum_orders = account_data.serum3_orders_by_raw_index(0);
assert_eq!(serum_orders.base_borrows_without_fee, 0);
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
assert!(order_id != 0);
//
// TEST: Cancel the order
//
send_tx(
solana,
Serum3CancelOrderInstruction {
side: Serum3Side::Bid,
order_id,
account,
owner,
serum_market,
},
)
.await
.unwrap();
order_placer.cancel(order_id).await;
//
// TEST: Settle, moving the freed up funds back
//
send_tx(
solana,
Serum3SettleFundsInstruction {
account,
owner,
serum_market,
},
)
.await
.unwrap();
order_placer.settle().await;
let native0 = account_position(solana, account, base_token.bank).await;
let native1 = account_position(solana, account, quote_token.bank).await;
assert_eq!(native0, 1000);
assert_eq!(native1, 1000);
// 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
// TODO: custom program error: 0x2a TooManyOpenOrders https://github.com/project-serum/serum-dex/blob/master/dex/src/error.rs#L88
// send_tx(
// solana,
// Serum3CloseOpenOrdersInstruction {
// account,
// serum_market,
// owner,
// sol_destination: payer.pubkey(),
// },
// )
// .await
// .unwrap();
send_tx(
solana,
Serum3CloseOpenOrdersInstruction {
account,
serum_market,
owner,
sol_destination: payer.pubkey(),
},
)
.await
.unwrap();
// deregister serum3 market
send_tx(
@ -203,3 +318,257 @@ async fn test_serum() -> Result<(), TransportError> {
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(())
}