Skip to content

Commit

Permalink
Check global trades are backed before swap (#119)
Browse files Browse the repository at this point in the history
* Global account check in swap

* Global account check in swap

* test for unbacked global

* client fix

* fmt

* switch global swap check to only the amount needed, not full

* move total base atoms matched

* move global

* todo
  • Loading branch information
brittcyr authored Sep 25, 2024
1 parent b7ec63e commit 3a8da83
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 9 deletions.
14 changes: 11 additions & 3 deletions client/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use hypertree::get_helper;
use manifest::{
quantities::{BaseAtoms, QuoteAtoms, WrapperU64},
state::{DynamicAccount, MarketFixed, MarketValue},
validation::{get_global_address, get_global_vault_address, get_vault_address},
validation::{
get_global_address, get_global_vault_address, get_vault_address,
loaders::GlobalTradeAccounts,
},
};
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
use std::mem::size_of;
Expand Down Expand Up @@ -91,12 +94,17 @@ impl Amm for ManifestMarket {

fn quote(&self, quote_params: &QuoteParams) -> Result<Quote> {
let market: DynamicAccount<MarketFixed, Vec<u8>> = self.market.clone();
let global_trade_accounts: &[Option<GlobalTradeAccounts>; 2] = &[None, None];
let out_amount: u64 = if quote_params.input_mint == self.get_base_mint() {
let in_atoms: BaseAtoms = BaseAtoms::new(quote_params.in_amount);
market.impact_quote_atoms(false, in_atoms)?.as_u64()
market
.impact_quote_atoms(false, in_atoms, global_trade_accounts)?
.as_u64()
} else {
let in_atoms: QuoteAtoms = QuoteAtoms::new(quote_params.in_amount);
market.impact_base_atoms(true, true, in_atoms)?.as_u64()
market
.impact_base_atoms(true, true, in_atoms, global_trade_accounts)?
.as_u64()
};
Ok(Quote {
out_amount,
Expand Down
14 changes: 12 additions & 2 deletions programs/manifest/src/program/processor/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,23 @@ pub(crate) fn process_swap(
} else {
// input=desired max(quote) output=checked min(base)
// round down base amount to not cross quote limit
dynamic_account.impact_base_atoms(true, false, QuoteAtoms::new(in_atoms))?
dynamic_account.impact_base_atoms(
true,
false,
QuoteAtoms::new(in_atoms),
&global_trade_accounts_opts,
)?
}
} else {
if is_base_in {
// input=checked max(base) output=desired min(quote)
// round up base amount to ensure not staying below quote limit
dynamic_account.impact_base_atoms(false, true, QuoteAtoms::new(out_atoms))?
dynamic_account.impact_base_atoms(
false,
true,
QuoteAtoms::new(out_atoms),
&global_trade_accounts_opts,
)?
} else {
// input=checked max(quote) output=desired min(base)
BaseAtoms::new(out_atoms)
Expand Down
4 changes: 3 additions & 1 deletion programs/manifest/src/state/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,9 @@ impl<Fixed: DerefOrBorrowMut<GlobalFixed>, Dynamic: DerefOrBorrowMut<[u8]>>
require!(
num_global_atoms <= global_atoms_deposited,
ManifestError::GlobalInsufficient,
"Insufficient funds for global order",
"Insufficient funds for global order needed {} has {}",
num_global_atoms,
global_atoms_deposited
)?;
}

Expand Down
35 changes: 33 additions & 2 deletions programs/manifest/src/state/market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ use super::{
constants::{MARKET_BLOCK_SIZE, MARKET_FIXED_SIZE},
order_type_can_rest,
utils::{
assert_already_has_seat, assert_not_already_expired, get_now_slot, try_to_add_to_global,
assert_already_has_seat, assert_not_already_expired, can_back_order, get_now_slot,
try_to_add_to_global,
},
DerefOrBorrow, DerefOrBorrowMut, DynamicAccount, RestingOrder, MARKET_FIXED_DISCRIMINANT,
MARKET_FREE_LIST_BLOCK_SIZE, NEXT_PLANNED_MAINTENANCE_SLOT,
Expand Down Expand Up @@ -285,6 +286,7 @@ impl<Fixed: DerefOrBorrow<MarketFixed>, Dynamic: DerefOrBorrow<[u8]>>
&self,
is_bid: bool,
limit_base_atoms: BaseAtoms,
_global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
) -> Result<QuoteAtoms, ProgramError> {
let now_slot: u32 = get_now_slot();

Expand All @@ -300,12 +302,14 @@ impl<Fixed: DerefOrBorrow<MarketFixed>, Dynamic: DerefOrBorrow<[u8]>>
if other_order.is_expired(now_slot) {
continue;
}

let matched_price = other_order.get_price();
let matched_base_atoms = other_order.get_num_base_atoms().min(remaining_base_atoms);
let matched_quote_atoms =
matched_price.checked_quote_for_base(matched_base_atoms, is_bid)?;

if other_order.get_order_type() == OrderType::Global {
// TODO: Check if the order is backed
}
total_quote_atoms_matched =
total_quote_atoms_matched.checked_add(matched_quote_atoms)?;
if matched_base_atoms == remaining_base_atoms {
Expand All @@ -327,6 +331,7 @@ impl<Fixed: DerefOrBorrow<MarketFixed>, Dynamic: DerefOrBorrow<[u8]>>
is_bid: bool,
round_up: bool,
limit_quote_atoms: QuoteAtoms,
global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
) -> Result<BaseAtoms, ProgramError> {
let now_slot: u32 = get_now_slot();

Expand All @@ -351,6 +356,32 @@ impl<Fixed: DerefOrBorrow<MarketFixed>, Dynamic: DerefOrBorrow<[u8]>>
let matched_quote_atoms =
matched_price.checked_quote_for_base(matched_base_atoms, is_bid)?;

// TODO: Clean this up into a separate function.
if other_order.get_order_type() == OrderType::Global {
// If global accounts are needed but not present, then this will
// crash. This is an intentional product decision. Would be
// valid to walk past, but we have chosen to give no fill rather
// than worse price if the taker takes the shortcut of not
// including global account.
let global_trade_accounts_opt: &Option<GlobalTradeAccounts> = if is_bid {
&global_trade_accounts_opts[0]
} else {
&global_trade_accounts_opts[1]
};
let has_enough_tokens: bool = can_back_order(
global_trade_accounts_opt,
self.get_trader_key_by_index(other_order.get_trader_index()),
GlobalAtoms::new(if is_bid {
matched_base_atoms.as_u64()
} else {
matched_quote_atoms.as_u64()
}),
);
if !has_enough_tokens {
continue;
}
}

total_base_atoms_matched = total_base_atoms_matched.checked_add(matched_base_atoms)?;

if matched_base_atoms == base_atoms_limit {
Expand Down
16 changes: 16 additions & 0 deletions programs/manifest/src/state/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,22 @@ pub(crate) fn assert_already_has_seat(trader_index: DataIndex) -> ProgramResult
Ok(())
}

pub(crate) fn can_back_order<'a, 'info>(
global_trade_accounts_opt: &'a Option<GlobalTradeAccounts<'a, 'info>>,
resting_order_trader: &Pubkey,
desired_global_atoms: GlobalAtoms,
) -> bool {
let global_trade_accounts: &GlobalTradeAccounts = &global_trade_accounts_opt.as_ref().unwrap();
let GlobalTradeAccounts { global, .. } = global_trade_accounts;

let global_data: &mut RefMut<&mut [u8]> = &mut global.try_borrow_mut_data().unwrap();
let global_dynamic_account: GlobalRefMut = get_mut_dynamic_account(global_data);

let num_deposited_atoms: GlobalAtoms =
global_dynamic_account.get_balance_atoms(resting_order_trader);
return desired_global_atoms <= num_deposited_atoms;
}

pub(crate) fn try_to_move_global_tokens<'a, 'info>(
global_trade_accounts_opt: &'a Option<GlobalTradeAccounts<'a, 'info>>,
resting_order_trader: &Pubkey,
Expand Down
124 changes: 123 additions & 1 deletion programs/manifest/tests/cases/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use borsh::BorshSerialize;
use manifest::{
program::{
batch_update::PlaceOrderParams, batch_update_instruction, global_add_trader_instruction,
global_deposit_instruction, swap_instruction, ManifestInstruction, SwapParams,
global_deposit_instruction, global_withdraw_instruction, swap_instruction,
ManifestInstruction, SwapParams,
},
state::{constants::NO_EXPIRATION_LAST_VALID_SLOT, OrderType},
validation::get_vault_address,
Expand Down Expand Up @@ -798,3 +799,124 @@ async fn swap_global() -> anyhow::Result<()> {

Ok(())
}

// Global is on the USDC, taker is sending in SOL. Global order is not backed,
// so the order does not get the global price.
#[tokio::test]
async fn swap_global_not_backed() -> anyhow::Result<()> {
let mut test_fixture: TestFixture = TestFixture::new().await;

let second_keypair: Keypair = test_fixture.second_keypair.insecure_clone();
test_fixture.claim_seat_for_keypair(&second_keypair).await?;

send_tx_with_retry(
Rc::clone(&test_fixture.context),
&[global_add_trader_instruction(
&test_fixture.global_fixture.key,
&second_keypair.pubkey(),
)],
Some(&second_keypair.pubkey()),
&[&second_keypair],
)
.await?;

// Make a throw away token account
let token_account_keypair: Keypair = Keypair::new();
let token_account_fixture: TokenAccountFixture = TokenAccountFixture::new_with_keypair(
Rc::clone(&test_fixture.context),
&test_fixture.global_fixture.mint_key,
&second_keypair.pubkey(),
&token_account_keypair,
)
.await;
test_fixture
.usdc_mint_fixture
.mint_to(&token_account_fixture.key, 2_000 * USDC_UNIT_SIZE)
.await;
send_tx_with_retry(
Rc::clone(&test_fixture.context),
&[global_deposit_instruction(
&test_fixture.global_fixture.mint_key,
&second_keypair.pubkey(),
&token_account_fixture.key,
&spl_token::id(),
2_000 * USDC_UNIT_SIZE,
)],
Some(&second_keypair.pubkey()),
&[&second_keypair],
)
.await?;
test_fixture
.deposit_for_keypair(Token::USDC, 1_000 * USDC_UNIT_SIZE, &second_keypair)
.await?;

let batch_update_ix: Instruction = batch_update_instruction(
&test_fixture.market_fixture.key,
&second_keypair.pubkey(),
None,
vec![],
vec![
PlaceOrderParams::new(
1 * SOL_UNIT_SIZE,
2,
0,
true,
OrderType::Global,
NO_EXPIRATION_LAST_VALID_SLOT,
),
PlaceOrderParams::new(
1 * SOL_UNIT_SIZE,
1,
0,
true,
OrderType::Limit,
NO_EXPIRATION_LAST_VALID_SLOT,
),
],
None,
None,
Some(*test_fixture.market_fixture.market.get_quote_mint()),
None,
);
send_tx_with_retry(
Rc::clone(&test_fixture.context),
&[batch_update_ix],
Some(&second_keypair.pubkey()),
&[&second_keypair],
)
.await?;

test_fixture
.sol_mint_fixture
.mint_to(&test_fixture.payer_sol_fixture.key, 1 * SOL_UNIT_SIZE)
.await;

assert_eq!(test_fixture.payer_usdc_fixture.balance_atoms().await, 0);

send_tx_with_retry(
Rc::clone(&test_fixture.context),
&[global_withdraw_instruction(
&test_fixture.global_fixture.mint_key,
&second_keypair.pubkey(),
&token_account_fixture.key,
&spl_token::id(),
2_000 * USDC_UNIT_SIZE,
)],
Some(&second_keypair.pubkey()),
&[&second_keypair],
)
.await?;

test_fixture
.swap_with_global(SOL_UNIT_SIZE, 1_000 * USDC_UNIT_SIZE, true, true)
.await?;

// Only get 1 out because the top of global is not backed.
assert_eq!(test_fixture.payer_sol_fixture.balance_atoms().await, 0);
assert_eq!(
test_fixture.payer_usdc_fixture.balance_atoms().await,
1_000 * USDC_UNIT_SIZE
);

Ok(())
}

0 comments on commit 3a8da83

Please sign in to comment.