PlaceSerumOrder: Track vault balances before and after

This commit is contained in:
Christian Kamm 2022-03-15 14:44:47 +01:00
parent 28a26e66da
commit baa980c659
8 changed files with 170 additions and 72 deletions

View File

@ -75,5 +75,17 @@ pub fn create_serum_open_orders(ctx: Context<CreateSerumOpenOrders>) -> Result<(
.create(serum_market.market_index)?; .create(serum_market.market_index)?;
oos.open_orders = ctx.accounts.open_orders.key(); oos.open_orders = ctx.accounts.open_orders.key();
// Make it so that the indexed_positions for the base and quote currency
// stay permanently blocked. Otherwise users may end up in situations where
// they can't settle a market because they don't have free indexed_positions!
let (quote_position, _) = account
.indexed_positions
.get_mut_or_create(serum_market.quote_token_index)?;
quote_position.in_use_count += 1;
let (base_position, _) = account
.indexed_positions
.get_mut_or_create(serum_market.base_token_index)?;
base_position.in_use_count += 1;
Ok(()) Ok(())
} }

View File

@ -1,14 +1,16 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_spl::dex;
use anchor_spl::token::{Token, TokenAccount}; use anchor_spl::token::{Token, TokenAccount};
use arrayref::array_refs; use arrayref::array_refs;
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use dex::serum_dex;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use serum_dex::matching::Side;
use std::io::Write; use std::io::Write;
use std::num::NonZeroU64; use std::num::NonZeroU64;
use anchor_spl::dex;
use dex::serum_dex;
use serum_dex::instruction::NewOrderInstructionV3;
use serum_dex::matching::Side;
use crate::error::*; use crate::error::*;
use crate::state::*; use crate::state::*;
@ -163,50 +165,101 @@ pub fn place_serum_order(
ctx: Context<PlaceSerumOrder>, ctx: Context<PlaceSerumOrder>,
order: NewOrderInstructionData, order: NewOrderInstructionData,
) -> Result<()> { ) -> Result<()> {
let account = ctx.accounts.account.load()?; //
let serum_market = ctx.accounts.serum_market.load()?; // Validation
//
{
let account = ctx.accounts.account.load()?;
let serum_market = ctx.accounts.serum_market.load()?;
// Validate open_orders // Validate open_orders
require!( require!(
account account
.serum_open_orders_map .serum_open_orders_map
.find(serum_market.market_index) .find(serum_market.market_index)
.ok_or(error!(MangoError::SomeError))? .ok_or(error!(MangoError::SomeError))?
.open_orders .open_orders
== ctx.accounts.open_orders.key(), == ctx.accounts.open_orders.key(),
MangoError::SomeError MangoError::SomeError
); );
// Validate banks and vaults // Validate banks and vaults
let quote_bank = ctx.accounts.quote_bank.load()?; let quote_bank = ctx.accounts.quote_bank.load()?;
require!( require!(
quote_bank.vault == ctx.accounts.quote_vault.key(), quote_bank.vault == ctx.accounts.quote_vault.key(),
MangoError::SomeError MangoError::SomeError
); );
require!( require!(
quote_bank.token_index == serum_market.quote_token_index, quote_bank.token_index == serum_market.quote_token_index,
MangoError::SomeError MangoError::SomeError
); );
let base_bank = ctx.accounts.base_bank.load()?; let base_bank = ctx.accounts.base_bank.load()?;
require!( require!(
base_bank.vault == ctx.accounts.base_vault.key(), base_bank.vault == ctx.accounts.base_vault.key(),
MangoError::SomeError MangoError::SomeError
); );
require!( require!(
base_bank.token_index == serum_market.base_token_index, base_bank.token_index == serum_market.base_token_index,
MangoError::SomeError MangoError::SomeError
); );
}
//
// Before-order tracking
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
// TODO: pre-health check // TODO: pre-health check
// TODO: track vault balance before
// //
// Place the order // Place the order
// //
cpi_place_order(&ctx, &order.0)?;
// unwrap our newtype // TODO: immediately call settle_funds?
let order = order.0;
//
// After-order tracking
//
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;
// Charge 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 (base_position, _) = account
.indexed_positions
.get_mut_or_create(base_bank.token_index)?;
base_bank.change(base_position, (after_base_vault - before_base_vault) as i64)?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let (quote_position, _) = account
.indexed_positions
.get_mut_or_create(quote_bank.token_index)?;
quote_bank.change(
quote_position,
(after_quote_vault - before_quote_vault) as i64,
)?;
}
//
// Health check
//
let account = ctx.accounts.account.load()?;
let health = compute_health(&account, &ctx.remaining_accounts)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::SomeError);
Ok(())
}
fn cpi_place_order(ctx: &Context<PlaceSerumOrder>, order: &NewOrderInstructionV3) -> Result<()> {
let order_payer_token_account = match order.side { let order_payer_token_account = match order.side {
Side::Bid => ctx.accounts.quote_vault.to_account_info(), Side::Bid => ctx.accounts.quote_vault.to_account_info(),
Side::Ask => ctx.accounts.base_vault.to_account_info(), Side::Ask => ctx.accounts.base_vault.to_account_info(),
@ -248,16 +301,5 @@ pub fn place_serum_order(
order.limit, order.limit,
)?; )?;
// TODO: immediately call settle_funds?
// TODO: track vault balance after, apply to user position
//
// Health check
//
let account = ctx.accounts.account.load()?;
let health = compute_health(&account, &ctx.remaining_accounts)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::SomeError);
Ok(()) Ok(())
} }

View File

@ -57,7 +57,7 @@ impl Bank {
self.indexed_total_borrows = cm!(self.indexed_total_borrows - indexed_change); self.indexed_total_borrows = cm!(self.indexed_total_borrows - indexed_change);
position.indexed_value = cm!(position.indexed_value + indexed_change); position.indexed_value = cm!(position.indexed_value + indexed_change);
return Ok(true); return Ok(true);
} else if new_native_position < I80F48::ONE { } else if new_native_position < I80F48::ONE && !position.is_in_use() {
// if there's less than one token deposited, zero the position // if there's less than one token deposited, zero the position
self.dust = cm!(self.dust + new_native_position); self.dust = cm!(self.dust + new_native_position);
self.indexed_total_borrows = self.indexed_total_borrows =
@ -93,7 +93,7 @@ impl Bank {
let new_native_position = cm!(native_position - native_amount); let new_native_position = cm!(native_position - native_amount);
if !new_native_position.is_negative() { if !new_native_position.is_negative() {
// withdraw deposits only // withdraw deposits only
if new_native_position < I80F48::ONE { if new_native_position < I80F48::ONE && !position.is_in_use() {
// zero the account collecting the leftovers in `dust` // zero the account collecting the leftovers in `dust`
self.dust = cm!(self.dust + new_native_position); self.dust = cm!(self.dust + new_native_position);
self.indexed_total_deposits = self.indexed_total_deposits =
@ -101,7 +101,7 @@ impl Bank {
position.indexed_value = I80F48::ZERO; position.indexed_value = I80F48::ZERO;
return Ok(false); return Ok(false);
} else { } else {
// withdraw some deposits leaving >1 native token // withdraw some deposits leaving a positive balance
let indexed_change = cm!(native_amount / self.deposit_index); let indexed_change = cm!(native_amount / self.deposit_index);
self.indexed_total_deposits = cm!(self.indexed_total_deposits - indexed_change); self.indexed_total_deposits = cm!(self.indexed_total_deposits - indexed_change);
position.indexed_value = cm!(position.indexed_value - indexed_change); position.indexed_value = cm!(position.indexed_value - indexed_change);
@ -123,4 +123,12 @@ impl Bank {
Ok(true) Ok(true)
} }
pub fn change(&mut self, position: &mut IndexedPosition, native_amount: i64) -> Result<bool> {
if native_amount >= 0 {
self.deposit(position, native_amount as u64)
} else {
self.withdraw(position, (-native_amount) as u64)
}
}
} }

View File

@ -19,6 +19,9 @@ pub struct IndexedPosition {
/// index into Group.tokens /// index into Group.tokens
pub token_index: TokenIndex, pub token_index: TokenIndex,
/// incremented when a market requires this position to stay alive
pub in_use_count: u8,
} }
// TODO: static assert the size and alignment // TODO: static assert the size and alignment
@ -38,6 +41,10 @@ impl IndexedPosition {
self.indexed_value * bank.borrow_index self.indexed_value * bank.borrow_index
} }
} }
pub fn is_in_use(&self) -> bool {
self.in_use_count > 0
}
} }
#[zero_copy] #[zero_copy]
@ -51,6 +58,7 @@ impl IndexedPositions {
values: [IndexedPosition { values: [IndexedPosition {
indexed_value: I80F48::ZERO, indexed_value: I80F48::ZERO,
token_index: TokenIndex::MAX, token_index: TokenIndex::MAX,
in_use_count: 0,
}; MAX_INDEXED_POSITIONS], }; MAX_INDEXED_POSITIONS],
} }
} }
@ -79,6 +87,7 @@ impl IndexedPositions {
self.values[i] = IndexedPosition { self.values[i] = IndexedPosition {
indexed_value: I80F48::ZERO, indexed_value: I80F48::ZERO,
token_index: token_index, token_index: token_index,
in_use_count: 0,
}; };
} }
} }
@ -90,12 +99,19 @@ impl IndexedPositions {
} }
pub fn deactivate(&mut self, index: usize) { pub fn deactivate(&mut self, index: usize) {
assert!(self.values[index].in_use_count == 0);
self.values[index].token_index = TokenIndex::MAX; self.values[index].token_index = TokenIndex::MAX;
} }
pub fn iter_active(&self) -> impl Iterator<Item = &IndexedPosition> { pub fn iter_active(&self) -> impl Iterator<Item = &IndexedPosition> {
self.values.iter().filter(|p| p.is_active()) self.values.iter().filter(|p| p.is_active())
} }
pub fn find(&self, token_index: TokenIndex) -> Option<&IndexedPosition> {
self.values
.iter()
.find(|p| p.is_active_for_token(token_index))
}
} }
#[zero_copy] #[zero_copy]

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::{self, SysvarId}; use anchor_lang::solana_program::sysvar::{self, SysvarId};
use anchor_spl::dex::serum_dex; use anchor_spl::dex::serum_dex;
@ -153,6 +155,17 @@ fn from_serum_style_pubkey(d: &[u64; 4]) -> Pubkey {
Pubkey::new(bytemuck::cast_slice(d as &[_])) Pubkey::new(bytemuck::cast_slice(d as &[_]))
} }
pub async fn account_position(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> i64 {
let account_data: MangoAccount = solana.get_account(account).await;
let bank_data: Bank = solana.get_account(bank).await;
let native = account_data
.indexed_positions
.find(bank_data.token_index)
.unwrap()
.native(&bank_data);
native.round().to_num::<i64>()
}
// //
// a struct for each instruction along with its // a struct for each instruction along with its
// ClientInstruction impl // ClientInstruction impl

View File

@ -114,13 +114,11 @@ async fn test_basic() -> Result<(), TransportError> {
solana.token_account_balance(payer_mint0_account).await, solana.token_account_balance(payer_mint0_account).await,
start_balance - deposit_amount start_balance - deposit_amount
); );
let account_data: MangoAccount = solana.get_account(account).await; assert_eq!(
let bank_data: Bank = solana.get_account(bank).await; account_position(solana, account, bank).await,
assert!( deposit_amount as i64
account_data.indexed_positions.values[0].native(&bank_data)
- I80F48::from_num(deposit_amount)
< dust_threshold
); );
let bank_data: Bank = solana.get_account(bank).await;
assert!( assert!(
bank_data.native_total_deposits() - I80F48::from_num(deposit_amount) < dust_threshold bank_data.native_total_deposits() - I80F48::from_num(deposit_amount) < dust_threshold
); );
@ -130,6 +128,7 @@ async fn test_basic() -> Result<(), TransportError> {
// TEST: Withdraw funds // TEST: Withdraw funds
// //
{ {
let start_amount = 100;
let withdraw_amount = 50; let withdraw_amount = 50;
let start_balance = solana.token_account_balance(payer_mint0_account).await; let start_balance = solana.token_account_balance(payer_mint0_account).await;
@ -151,16 +150,15 @@ async fn test_basic() -> Result<(), TransportError> {
solana.token_account_balance(payer_mint0_account).await, solana.token_account_balance(payer_mint0_account).await,
start_balance + withdraw_amount start_balance + withdraw_amount
); );
let account_data: MangoAccount = solana.get_account(account).await; assert_eq!(
account_position(solana, account, bank).await,
(start_amount - withdraw_amount) as i64
);
let bank_data: Bank = solana.get_account(bank).await; let bank_data: Bank = solana.get_account(bank).await;
assert!( assert!(
account_data.indexed_positions.values[0].native(&bank_data) bank_data.native_total_deposits() - I80F48::from_num(start_amount - withdraw_amount)
- I80F48::from_num(withdraw_amount)
< dust_threshold < dust_threshold
); );
assert!(
bank_data.native_total_deposits() - I80F48::from_num(withdraw_amount) < dust_threshold
);
} }
Ok(()) Ok(())

View File

@ -109,7 +109,7 @@ async fn test_position_lifetime() -> Result<()> {
(oracle, bank) (oracle, bank)
}; };
register_mint(0, mint0.clone()).await; register_mint(0, mint0.clone()).await;
register_mint(1, mint1.clone()).await; let (_oracle1, bank1) = register_mint(1, mint1.clone()).await;
register_mint(2, mint2.clone()).await; register_mint(2, mint2.clone()).await;
// //
@ -217,6 +217,10 @@ async fn test_position_lifetime() -> Result<()> {
) )
.await .await
.unwrap(); .unwrap();
assert_eq!(
account_position(solana, account, bank1).await,
-(borrow_amount as i64)
);
// give it back, closing the position // give it back, closing the position
send_tx( send_tx(

View File

@ -98,17 +98,17 @@ async fn test_serum() -> Result<(), TransportError> {
let address_lookup_table = solana.create_address_lookup_table(admin, payer).await; let address_lookup_table = solana.create_address_lookup_table(admin, payer).await;
let base_token_index = 0; let base_token_index = 0;
let (_oracle0, _bank0) = let (_oracle0, bank0) =
register_mint(base_token_index, mint0.clone(), address_lookup_table).await; register_mint(base_token_index, mint0.clone(), address_lookup_table).await;
let quote_token_index = 1; let quote_token_index = 1;
let (_oracle1, _bank1) = let (_oracle1, bank1) =
register_mint(quote_token_index, mint1.clone(), address_lookup_table).await; register_mint(quote_token_index, mint1.clone(), address_lookup_table).await;
// //
// SETUP: Deposit user funds // SETUP: Deposit user funds
// //
{ {
let deposit_amount = 100; let deposit_amount = 1000;
send_tx( send_tx(
solana, solana,
@ -187,12 +187,12 @@ async fn test_serum() -> Result<(), TransportError> {
send_tx( send_tx(
solana, solana,
PlaceSerumOrderInstruction { PlaceSerumOrderInstruction {
side: 0, side: 0, // TODO: Bid
limit_price: 1, limit_price: 10, // in quote_lot (10) per base lot (100)
max_base_qty: 1, max_base_qty: 1, // in base lot (100)
max_native_quote_qty_including_fees: 1, max_native_quote_qty_including_fees: 100,
self_trade_behavior: 0, self_trade_behavior: 0,
order_type: 0, order_type: 0, // TODO: Limit
client_order_id: 0, client_order_id: 0,
limit: 10, limit: 10,
account, account,
@ -203,5 +203,10 @@ async fn test_serum() -> Result<(), TransportError> {
.await .await
.unwrap(); .unwrap();
let native0 = account_position(solana, account, bank0).await;
let native1 = account_position(solana, account, bank1).await;
assert_eq!(native0, 1000);
assert_eq!(native1, 900);
Ok(()) Ok(())
} }