PlaceSerumOrder: Track vault balances before and after
This commit is contained in:
parent
28a26e66da
commit
baa980c659
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue