diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index c5b224fe22..269b7aa59a 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,3 +1,5 @@ +use anchor_lang::Discriminator; +use anchor_spl::associated_token::AssociatedToken; use std::convert::identity; use std::mem::size_of; @@ -15,10 +17,14 @@ use phoenix::quantities::WrapperU64; use pyth_solana_receiver_sdk::cpi::accounts::InitPriceUpdate; use pyth_solana_receiver_sdk::program::PythSolanaReceiver; use serum_dex::state::ToAlignedBytes; +use solana_program::sysvar::instructions; -use crate::controller::token::close_vault; +use crate::controller::token::{close_vault, receive, send_from_program_vault}; use crate::error::ErrorCode; -use crate::ids::admin_hot_wallet; +use crate::ids::{ + admin_hot_wallet, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, lighthouse, + marinade_mainnet, serum_program, +}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; @@ -27,8 +33,8 @@ use crate::math::constants::{ IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, - QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, - SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, + PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, }; use crate::math::cp_curve::get_update_k_result; use crate::math::orders::is_multiple_of_step_size; @@ -84,6 +90,8 @@ use crate::{load_mut, PTYH_PRICE_FEED_SEED_PREFIX}; use crate::{math, safe_decrement, safe_increment}; use crate::{math_error, SPOT_BALANCE_PRECISION}; +use super::optional_accounts::get_token_interface; + pub fn handle_initialize(ctx: Context) -> Result<()> { let (drift_signer, drift_signer_nonce) = Pubkey::find_program_address(&[b"drift_signer".as_ref()], ctx.program_id); @@ -4730,8 +4738,8 @@ pub fn handle_initialize_constituent<'info>( swap_fee_min: i64, swap_fee_max: i64, oracle_staleness_threshold: u64, - beta: i32, cost_to_trade_bps: i32, + stablecoin_weight: u64, ) -> Result<()> { let mut constituent = ctx.accounts.constituent.load_init()?; let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; @@ -4747,7 +4755,6 @@ pub fn handle_initialize_constituent<'info>( .targets .get_mut(current_len) .unwrap(); - new_target.beta = beta; new_target.cost_to_trade_bps = cost_to_trade_bps; constituent_target_base.validate()?; @@ -4757,6 +4764,12 @@ pub fn handle_initialize_constituent<'info>( spot_market_index ); + validate!( + stablecoin_weight >= 0 && stablecoin_weight <= PRICE_PRECISION_U64, + ErrorCode::InvalidConstituent, + "stablecoin_weight must be between 0 and 1", + )?; + constituent.spot_market_index = spot_market_index; constituent.constituent_index = lp_pool.constituents; constituent.decimals = decimals; @@ -4770,6 +4783,7 @@ pub fn handle_initialize_constituent<'info>( constituent.lp_pool = lp_pool.pubkey; constituent.constituent_index = (constituent_target_base.targets.len() - 1) as u16; constituent.next_swap_id = 1; + constituent.stablecoin_weight = stablecoin_weight; lp_pool.constituents += 1; Ok(()) @@ -4781,8 +4795,8 @@ pub struct ConstituentParams { pub swap_fee_min: Option, pub swap_fee_max: Option, pub oracle_staleness_threshold: Option, - pub beta: Option, pub cost_to_trade_bps: Option, + pub stablecoin_weight: Option, } pub fn handle_update_constituent_params<'info>( @@ -4827,7 +4841,7 @@ pub fn handle_update_constituent_params<'info>( constituent_params.oracle_staleness_threshold.unwrap(); } - if constituent_params.beta.is_some() || constituent_params.cost_to_trade_bps.is_some() { + if constituent_params.cost_to_trade_bps.is_some() { let constituent_target_base = &mut ctx.accounts.constituent_target_base; let target = constituent_target_base @@ -4835,19 +4849,21 @@ pub fn handle_update_constituent_params<'info>( .get_mut(constituent.constituent_index as usize) .unwrap(); - if constituent_params.cost_to_trade_bps.is_some() { - msg!( - "cost_to_trade: {:?} -> {:?}", - target.cost_to_trade_bps, - constituent_params.cost_to_trade_bps - ); - target.cost_to_trade_bps = constituent_params.cost_to_trade_bps.unwrap(); - } + msg!( + "cost_to_trade: {:?} -> {:?}", + target.cost_to_trade_bps, + constituent_params.cost_to_trade_bps + ); + target.cost_to_trade_bps = constituent_params.cost_to_trade_bps.unwrap(); + } - if constituent_params.beta.is_some() { - msg!("beta: {:?} -> {:?}", target.beta, constituent_params.beta); - target.beta = constituent_params.beta.unwrap(); - } + if constituent_params.stablecoin_weight.is_some() { + msg!( + "stablecoin_weight: {:?} -> {:?}", + constituent.stablecoin_weight, + constituent_params.stablecoin_weight + ); + constituent.stablecoin_weight = constituent_params.stablecoin_weight.unwrap(); } Ok(()) @@ -4970,6 +4986,309 @@ pub fn handle_add_amm_constituent_data<'info>( Ok(()) } +pub fn handle_begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + let ixs = ctx.accounts.instructions.as_ref(); + let current_index = instructions::load_current_index_checked(ixs)? as usize; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + validate!( + mint.is_some(), + ErrorCode::InvalidSwap, + "BeginLpSwap must have a mint for in token passed in" + )?; + let mint = mint.unwrap(); + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let current_ix = instructions::load_instruction_at_checked(current_index, ixs)?; + validate!( + current_ix.program_id == *ctx.program_id, + ErrorCode::InvalidSwap, + "SwapBegin must be a top-level instruction (cant be cpi)" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSwap, + "in and out market the same" + )?; + + validate!( + amount_in != 0, + ErrorCode::InvalidSwap, + "amount_in cannot be zero" + )?; + + // Validate that the passed mint is accpetable + let mint_key = mint.key(); + validate!( + mint_key == ctx.accounts.constituent_in_token_account.mint, + ErrorCode::InvalidSwap, + "mint passed to SwapBegin does not match the mint constituent in token account" + )?; + + // Make sure we have enough balance to do the swap + let constituent_in_token_account = &ctx.accounts.constituent_in_token_account; + let constituent_out_token_account = &ctx.accounts.constituent_out_token_account; + + validate!( + amount_in <= constituent_in_token_account.amount, + ErrorCode::InvalidSwap, + "trying to swap more than the balance of the constituent in token account" + )?; + + validate!( + out_constituent.flash_loan_initial_token_amount == 0, + ErrorCode::InvalidSwap, + "begin_lp_swap ended in invalid state" + )?; + + in_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_in_token_account.amount; + out_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_out_token_account.amount; + + send_from_program_vault( + &ctx.accounts.token_program, + constituent_in_token_account, + &ctx.accounts.signer_in_token_account, + &ctx.accounts.drift_signer.to_account_info(), + state.signer_nonce, + amount_in, + &Some(mint), + )?; + + // The only other drift program allowed is SwapEnd + let mut index = current_index + 1; + let mut found_end = false; + loop { + let ix = match instructions::load_instruction_at_checked(index, ixs) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, + Err(e) => return Err(e.into()), + }; + + // Check that the drift program key is not used + if ix.program_id == crate::id() { + // must be the last ix -- this could possibly be relaxed + validate!( + !found_end, + ErrorCode::InvalidSwap, + "the transaction must not contain a Drift instruction after FlashLoanEnd" + )?; + found_end = true; + + // must be the SwapEnd instruction + let discriminator = crate::instruction::EndLpSwap::discriminator(); + validate!( + ix.data[0..8] == discriminator, + ErrorCode::InvalidSwap, + "last drift ix must be end of swap" + )?; + + validate!( + ctx.accounts.signer_out_token_account.key() == ix.accounts[2].pubkey, + ErrorCode::InvalidSwap, + "the out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.signer_in_token_account.key() == ix.accounts[3].pubkey, + ErrorCode::InvalidSwap, + "the in_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_out_token_account.key() == ix.accounts[4].pubkey, + ErrorCode::InvalidSwap, + "the constituent out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_in_token_account.key() == ix.accounts[5].pubkey, + ErrorCode::InvalidSwap, + "the constituent in token account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.out_constituent.key() == ix.accounts[6].pubkey, + ErrorCode::InvalidSwap, + "the out constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.in_constituent.key() == ix.accounts[7].pubkey, + ErrorCode::InvalidSwap, + "the in constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.lp_pool.key() == ix.accounts[8].pubkey, + ErrorCode::InvalidSwap, + "the lp pool passed to SwapBegin and End must match" + )?; + } else { + if found_end { + if ix.program_id == lighthouse::ID { + continue; + } + + for meta in ix.accounts.iter() { + validate!( + meta.is_writable == false, + ErrorCode::InvalidSwap, + "instructions after swap end must not have writable accounts" + )?; + } + } else { + let mut whitelisted_programs = vec![ + serum_program::id(), + AssociatedToken::id(), + jupiter_mainnet_3::ID, + jupiter_mainnet_4::ID, + jupiter_mainnet_6::ID, + ]; + whitelisted_programs.push(Token::id()); + whitelisted_programs.push(Token2022::id()); + whitelisted_programs.push(marinade_mainnet::ID); + + validate!( + whitelisted_programs.contains(&ix.program_id), + ErrorCode::InvalidSwap, + "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + )?; + + for meta in ix.accounts.iter() { + validate!( + meta.pubkey != crate::id(), + ErrorCode::InvalidSwap, + "instructions between begin and end must not be drift instructions" + )?; + } + } + } + + index += 1; + } + + validate!( + found_end, + ErrorCode::InvalidSwap, + "found no SwapEnd instruction in transaction" + )?; + + Ok(()) +} + +pub fn handle_end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, +) -> Result<()> { + let signer_in_token_account = &ctx.accounts.signer_in_token_account; + let signer_out_token_account = &ctx.accounts.signer_out_token_account; + + let admin_account_info = ctx.accounts.admin.to_account_info(); + + let constituent_in_token_account = &ctx.accounts.constituent_in_token_account; + let constituent_out_token_account = &ctx.accounts.constituent_out_token_account; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let out_token_program = get_token_interface(remaining_accounts)?; + + let in_mint = get_token_mint(remaining_accounts)?; + let out_mint = get_token_mint(remaining_accounts)?; + + validate!( + in_mint.is_some(), + ErrorCode::InvalidSwap, + "EndLpSwap must have a mint for in token passed in" + )?; + + validate!( + out_mint.is_some(), + ErrorCode::InvalidSwap, + "EndLpSwap must have a mint for out token passed in" + )?; + + let in_mint = in_mint.unwrap(); + let out_mint = out_mint.unwrap(); + + // Validate that the passed mint is accpetable + let mint_key = out_mint.key(); + validate!( + mint_key == constituent_out_token_account.mint, + ErrorCode::InvalidSwap, + "mint passed to EndLpSwap does not match the mint constituent out token account" + )?; + + let mint_key = in_mint.key(); + validate!( + mint_key == constituent_in_token_account.mint, + ErrorCode::InvalidSwap, + "mint passed to EndLpSwap does not match the mint constituent in token account" + )?; + + // Residual of what wasnt swapped + if signer_in_token_account.amount > in_constituent.flash_loan_initial_token_amount { + let residual = signer_in_token_account + .amount + .safe_sub(in_constituent.flash_loan_initial_token_amount)?; + + controller::token::receive( + &ctx.accounts.token_program, + signer_in_token_account, + constituent_in_token_account, + &admin_account_info, + residual, + &Some(in_mint), + )?; + } + + // Whatever was swapped + if signer_out_token_account.amount > out_constituent.flash_loan_initial_token_amount { + let residual = signer_out_token_account + .amount + .safe_sub(out_constituent.flash_loan_initial_token_amount)?; + + if let Some(token_interface) = out_token_program { + receive( + &token_interface, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &Some(out_mint), + )?; + } else { + receive( + &ctx.accounts.token_program, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &Some(out_mint), + )?; + } + } + + // Update the balance on the token accounts for after swap + out_constituent.sync_token_balance(constituent_out_token_account.amount); + in_constituent.sync_token_balance(constituent_in_token_account.amount); + + out_constituent.flash_loan_initial_token_amount = 0; + in_constituent.flash_loan_initial_token_amount = 0; + + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5851,7 +6170,6 @@ pub struct InitializeLpPool<'info> { #[derive(Accounts)] #[instruction( - lp_pool_name: [u8; 32], spot_market_index: u16, )] pub struct InitializeConstituent<'info> { @@ -5863,11 +6181,7 @@ pub struct InitializeConstituent<'info> { )] pub admin: Signer<'info>, - #[account( - mut, - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump = lp_pool.load()?.bump, - )] + #[account(mut)] pub lp_pool: AccountLoader<'info, LPPool>, #[account( @@ -5909,15 +6223,7 @@ pub struct InitializeConstituent<'info> { } #[derive(Accounts)] -#[instruction( - lp_pool_name: [u8; 32], -)] pub struct UpdateConstituentParams<'info> { - #[account( - mut, - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump = lp_pool.load()?.bump, - )] pub lp_pool: AccountLoader<'info, LPPool>, #[account( mut, @@ -5945,7 +6251,6 @@ pub struct AddAmmConstituentMappingDatum { #[derive(Accounts)] #[instruction( - lp_pool_name: [u8; 32], amm_constituent_mapping_data: Vec, )] pub struct AddAmmConstituentMappingData<'info> { @@ -5954,11 +6259,6 @@ pub struct AddAmmConstituentMappingData<'info> { constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, - - #[account( - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump, - )] pub lp_pool: AccountLoader<'info, LPPool>, #[account( @@ -5985,7 +6285,6 @@ pub struct AddAmmConstituentMappingData<'info> { #[derive(Accounts)] #[instruction( - lp_pool_name: [u8; 32], amm_constituent_mapping_data: Vec, )] pub struct UpdateAmmConstituentMappingData<'info> { @@ -5994,11 +6293,6 @@ pub struct UpdateAmmConstituentMappingData<'info> { constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, - - #[account( - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump, - )] pub lp_pool: AccountLoader<'info, LPPool>, #[account( @@ -6012,20 +6306,12 @@ pub struct UpdateAmmConstituentMappingData<'info> { } #[derive(Accounts)] -#[instruction( - lp_pool_name: [u8; 32], -)] pub struct RemoveAmmConstituentMappingData<'info> { #[account( mut, constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, - - #[account( - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump, - )] pub lp_pool: AccountLoader<'info, LPPool>, #[account( @@ -6040,3 +6326,72 @@ pub struct RemoveAmmConstituentMappingData<'info> { pub system_program: Program<'info, System>, pub state: Box>, } + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPTakerSwap<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + + /// Signer token accounts + #[account( + mut, + constraint = &constituent_out_token_account.mint.eq(&signer_out_token_account.mint), + token::authority = admin + )] + pub signer_out_token_account: Box>, + #[account( + mut, + constraint = &constituent_in_token_account.mint.eq(&signer_in_token_account.mint), + token::authority = admin + )] + pub signer_in_token_account: Box>, + + /// Constituent token accounts + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + constraint = &out_constituent.load()?.mint.eq(&constituent_out_token_account.mint), + token::authority = drift_signer + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + constraint = &in_constituent.load()?.mint.eq(&constituent_in_token_account.mint), + token::authority = drift_signer + )] + pub constituent_in_token_account: Box>, + + /// Constituents + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump = out_constituent.load()?.bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump = in_constituent.load()?.bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// Instructions Sysvar for instruction introspection + /// CHECK: fixed instructions sysvar account + #[account(address = instructions::ID)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, +} diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index efd6fcb8dc..30ba2cfa9d 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -7,7 +7,7 @@ use crate::{ get_then_update_id, math::{ casting::Cast, - constants::PRICE_PRECISION_I128, + constants::{PERCENTAGE_PRECISION_I128, PRICE_PRECISION_I128}, oracle::{is_oracle_valid_for_action, oracle_validity, DriftAction}, safe_math::SafeMath, }, @@ -43,7 +43,6 @@ use crate::state::lp_pool::{ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, - lp_pool_name: [u8; 32], constituent_indexes: Vec, ) -> Result<()> { let lp_pool = &ctx.accounts.lp_pool.load()?; @@ -71,22 +70,6 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); let amm_mapping_key = &ctx.accounts.amm_constituent_mapping.key(); - // Validate lp pool pda - let expected_lp_pda = &Pubkey::create_program_address( - &[ - b"lp_pool", - lp_pool_name.as_ref(), - lp_pool.bump.to_le_bytes().as_ref(), - ], - &crate::ID, - ) - .map_err(|_| ErrorCode::InvalidPDA)?; - validate!( - expected_lp_pda.eq(lp_pool_key), - ErrorCode::InvalidPDA, - "Lp pool PDA does not match expected PDA" - )?; - let mut constituent_target_base: AccountZeroCopyMut< '_, TargetsDatum, @@ -228,8 +211,31 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( "Constituent map length does not match lp pool constituent count" )?; + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + let expected_pda = &Pubkey::create_program_address( + &[ + CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), + lp_pool.pubkey.as_ref(), + constituent_target_base.fixed.bump.to_le_bytes().as_ref(), + ], + &crate::ID, + ) + .map_err(|_| ErrorCode::InvalidPDA)?; + validate!( + expected_pda.eq(constituent_target_base_key), + ErrorCode::InvalidPDA, + "Constituent target weights PDA does not match expected PDA" + )?; + let mut aum: u128 = 0; + let mut crypto_delta = 0_i128; let mut oldest_slot = u64::MAX; + let mut stablecoin_constituent_indexes: Vec = vec![]; for i in 0..lp_pool.constituents as usize { let mut constituent = constituent_map.get_ref_mut(&(i as u16))?; @@ -286,6 +292,11 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( .safe_mul(oracle_price.unwrap() as i128)? .safe_div(PRICE_PRECISION_I128)? .max(0); + if constituent.stablecoin_weight == 0 { + crypto_delta = crypto_delta.safe_add(constituent_aum.cast()?)?; + } else { + stablecoin_constituent_indexes.push(i); + } aum = aum.safe_add(constituent_aum.cast()?)?; } @@ -294,6 +305,16 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( lp_pool.last_aum_slot = slot; lp_pool.last_aum_ts = Clock::get()?.unix_timestamp; + let total_stable_target_base = aum.cast::()?.safe_sub(crypto_delta.abs())?; + for index in stablecoin_constituent_indexes { + let constituent = constituent_map.get_ref(&(index as u16))?; + let stable_target = constituent_target_base.get_mut(index as u32); + stable_target.target_base = total_stable_target_base + .safe_mul(constituent.stablecoin_weight as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .cast::()?; + } + Ok(()) } @@ -892,9 +913,6 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( } #[derive(Accounts)] -#[instruction( - lp_pool_name: [u8; 32], -)] pub struct UpdateConstituentTargetBase<'info> { pub state: Box>, #[account(mut)] @@ -905,27 +923,19 @@ pub struct UpdateConstituentTargetBase<'info> { pub constituent_target_base: AccountInfo<'info>, /// CHECK: checked in AmmCacheZeroCopy checks pub amm_cache: AccountInfo<'info>, - #[account( - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump = lp_pool.load()?.bump, - )] pub lp_pool: AccountLoader<'info, LPPool>, } #[derive(Accounts)] -#[instruction( - lp_pool_name: [u8; 32], -)] pub struct UpdateLPPoolAum<'info> { pub state: Box>, #[account(mut)] pub keeper: Signer<'info>, - #[account( - mut, - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump, - )] + #[account(mut)] pub lp_pool: AccountLoader<'info, LPPool>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, } /// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account @@ -947,9 +957,17 @@ pub struct LPPoolSwap<'info> { /// CHECK: checked in ConstituentTargetBaseZeroCopy checks pub constituent_target_base: AccountInfo<'info>, - #[account(mut)] + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] pub constituent_in_token_account: Box>, - #[account(mut)] + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] pub constituent_out_token_account: Box>, #[account( @@ -995,18 +1013,13 @@ pub struct LPPoolSwap<'info> { #[derive(Accounts)] #[instruction( - lp_pool_name: [u8; 32], in_market_index: u16, )] pub struct LPPoolAddLiquidity<'info> { /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, pub state: Box>, - #[account( - mut, - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump, - )] + #[account(mut)] pub lp_pool: AccountLoader<'info, LPPool>, pub authority: Signer<'info>, pub in_market_mint: Box>, @@ -1025,7 +1038,11 @@ pub struct LPPoolAddLiquidity<'info> { )] pub user_in_token_account: Box>, - #[account(mut)] + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] pub constituent_in_token_account: Box>, #[account( @@ -1058,18 +1075,13 @@ pub struct LPPoolAddLiquidity<'info> { #[derive(Accounts)] #[instruction( - lp_pool_name: [u8; 32], in_market_index: u16, )] pub struct LPPoolRemoveLiquidity<'info> { /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, pub state: Box>, - #[account( - mut, - seeds = [b"lp_pool", lp_pool_name.as_ref()], - bump, - )] + #[account(mut)] pub lp_pool: AccountLoader<'info, LPPool>, pub authority: Signer<'info>, pub out_market_mint: Box>, @@ -1087,7 +1099,11 @@ pub struct LPPoolRemoveLiquidity<'info> { constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) )] pub user_out_token_account: Box>, - #[account(mut)] + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] pub constituent_out_token_account: Box>, #[account( mut, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index d2b0fddeba..61d940a648 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1739,15 +1739,14 @@ pub mod drift { pub fn initialize_constituent<'info>( ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, - lp_pool_name: [u8; 32], spot_market_index: u16, decimals: u8, max_weight_deviation: i64, swap_fee_min: i64, swap_fee_max: i64, oracle_staleness_threshold: u64, - beta: i32, cost_to_trade: i32, + stablecoin_weight: i64, ) -> Result<()> { handle_initialize_constituent( ctx, @@ -1757,14 +1756,13 @@ pub mod drift { swap_fee_min, swap_fee_max, oracle_staleness_threshold, - beta, cost_to_trade, + stablecoin_weight, ) } pub fn update_constituent_params( ctx: Context, - lp_pool_name: [u8; 32], constituent_params: ConstituentParams, ) -> Result<()> { handle_update_constituent_params(ctx, constituent_params) @@ -1772,7 +1770,6 @@ pub mod drift { pub fn add_amm_constituent_mapping_data( ctx: Context, - lp_pool_name: [u8; 32], amm_constituent_mapping_data: Vec, ) -> Result<()> { handle_add_amm_constituent_data(ctx, amm_constituent_mapping_data) @@ -1780,7 +1777,6 @@ pub mod drift { pub fn update_amm_constituent_mapping_data( ctx: Context, - lp_pool_name: [u8; 32], amm_constituent_mapping_data: Vec, ) -> Result<()> { handle_update_amm_constituent_mapping_data(ctx, amm_constituent_mapping_data) @@ -1788,7 +1784,6 @@ pub mod drift { pub fn remove_amm_constituent_mapping_data<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, RemoveAmmConstituentMappingData<'info>>, - lp_pool_name: [u8; 32], perp_market_index: u16, constituent_index: u16, ) -> Result<()> { @@ -1797,15 +1792,13 @@ pub mod drift { pub fn update_lp_constituent_target_base<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, - lp_pool_name: [u8; 32], constituent_indexes: Vec, ) -> Result<()> { - handle_update_constituent_target_base(ctx, lp_pool_name, constituent_indexes) + handle_update_constituent_target_base(ctx, constituent_indexes) } pub fn update_lp_pool_aum<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, - lp_pool_name: [u8; 32], ) -> Result<()> { handle_update_lp_pool_aum(ctx) } @@ -1834,7 +1827,6 @@ pub mod drift { pub fn lp_pool_add_liquidity<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, - _lp_pool_name: [u8; 32], in_market_index: u16, in_amount: u64, min_mint_amount: u64, @@ -1844,13 +1836,29 @@ pub mod drift { pub fn lp_pool_remove_liquidity<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, - _lp_pool_name: [u8; 32], in_market_index: u16, in_amount: u64, min_out_amount: u64, ) -> Result<()> { handle_lp_pool_remove_liquidity(ctx, in_market_index, in_amount, min_out_amount) } + + pub fn begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, + ) -> Result<()> { + handle_begin_lp_swap(ctx, in_market_index, out_market_index, amount_in) + } + + pub fn end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + ) -> Result<()> { + handle_end_lp_swap(ctx) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index bbaf6b4a2b..092cf466b0 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1,7 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ - PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, QUOTE_PRECISION, + BASE_PRECISION_I128, BASE_PRECISION_I64, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + PRICE_PRECISION_I64, QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; @@ -467,11 +468,14 @@ pub struct Constituent { /// Every swap to/from this constituent has a monotonically increasing id. This is the next id to use pub next_swap_id: u64, - _padding2: [u8; 8], + /// percentable of stablecoin weight to go to this specific stablecoin PERCENTAGE_PRECISION + /// ZERO for non-stablecoins + pub stablecoin_weight: u64, + pub flash_loan_initial_token_amount: u64, } impl Size for Constituent { - const SIZE: usize = 232; + const SIZE: usize = 240; } impl Constituent { @@ -632,10 +636,10 @@ pub struct WeightDatum { #[derive(Debug, Default, BorshDeserialize, BorshSerialize)] #[repr(C)] pub struct TargetsDatum { + pub cost_to_trade_bps: i32, + pub _padding: [u8; 4], pub last_slot: u64, pub target_base: i64, - pub beta: i32, - pub cost_to_trade_bps: i32, } #[zero_copy] @@ -731,7 +735,6 @@ impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { let datum = self.get(constituent_index as u32); let target_weight = calculate_target_weight( datum.target_base, - datum.beta, datum.cost_to_trade_bps, &spot_market, price, @@ -744,31 +747,18 @@ impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { pub fn calculate_target_weight( target_base: i64, - beta: i32, cost_to_trade_bps: i32, spot_market: &SpotMarket, price: i64, lp_pool_aum: u128, validation_flags: WeightValidationFlags, ) -> DriftResult { - //.clamp(-PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I128) as i64; - - // assumes PRICE_PRECISION = PERCENTAGE_PRECISION - let token_precision = 10_i128.pow(spot_market.decimals as u32); - - let value_usd = target_base - .cast::()? - .safe_mul(price.cast::()?)? - .safe_mul( - beta.cast::()? - .safe_mul(10000_i128)? - .safe_div(cost_to_trade_bps.cast::()?)?, - )?; + let notional = target_base.safe_mul(price)?.safe_div(BASE_PRECISION_I64)?; - let target_weight = value_usd + let target_weight = notional .cast::()? - .safe_mul(PERCENTAGE_PRECISION_I64.cast::()?)? - .safe_div(lp_pool_aum.cast::()?.safe_mul(token_precision)?)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(lp_pool_aum.cast::()?)? .cast::()?; // if (validation_flags as u8 & (WeightValidationFlags::NoNegativeWeights as u8) != 0) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 3ac7723f1d..bbcf375567 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1,4 +1,5 @@ import { + AddressLookupTableAccount, Keypair, LAMPORTS_PER_SOL, PublicKey, @@ -18,6 +19,7 @@ import { SpotFulfillmentConfigStatus, AddAmmConstituentMappingDatum, TxParams, + SwapReduceOnly, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -48,6 +50,7 @@ import { getConstituentVaultPublicKey, getAmmCachePublicKey, getLpPoolTokenVaultPublicKey, + getDriftSignerPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; import { @@ -70,6 +73,11 @@ import { PROGRAM_ID as PHOENIX_PROGRAM_ID } from '@ellipsis-labs/phoenix-sdk'; import { DRIFT_ORACLE_RECEIVER_ID } from './config'; import { getFeedIdUint8Array } from './util/pythOracleUtils'; import { FUEL_RESET_LOG_ACCOUNT } from './constants/txConstants'; +import { + JupiterClient, + QuoteResponse, + SwapMode, +} from './jupiter/jupiterClient'; const OPENBOOK_PROGRAM_ID = new PublicKey( 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' @@ -4463,8 +4471,8 @@ export class AdminClient extends DriftClient { swapFeeMin: BN, swapFeeMax: BN, oracleStalenessThreshold: BN, - beta: number, - costToTrade: number + costToTrade: number, + stablecoinWeight: BN ): Promise { const ixs = await this.getInitializeConstituentIx( lpPoolName, @@ -4474,8 +4482,8 @@ export class AdminClient extends DriftClient { swapFeeMin, swapFeeMax, oracleStalenessThreshold, - beta, - costToTrade + costToTrade, + stablecoinWeight ); const tx = await this.buildTransaction(ixs); const { txSig } = await this.sendTransaction(tx, []); @@ -4490,8 +4498,8 @@ export class AdminClient extends DriftClient { swapFeeMin: BN, swapFeeMax: BN, oracleStalenessThreshold: BN, - beta: number, - costToTrade: number + costToTrade: number, + stablecoinWeight: BN ): Promise { const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); const constituentTargetBase = getConstituentTargetBasePublicKey( @@ -4507,15 +4515,14 @@ export class AdminClient extends DriftClient { return [ this.program.instruction.initializeConstituent( - lpPoolName, spotMarketIndex, decimals, maxWeightDeviation, swapFeeMin, swapFeeMax, oracleStalenessThreshold, - beta, costToTrade, + stablecoinWeight, { accounts: { admin: this.wallet.publicKey, @@ -4548,8 +4555,8 @@ export class AdminClient extends DriftClient { swapFeeMin?: BN; swapFeeMax?: BN; oracleStalenessThreshold?: BN; - beta?: number; costToTradeBps?: number; + stablecoinWeight?: BN; } ): Promise { const ixs = await this.getUpdateConstituentParamsIx( @@ -4570,22 +4577,21 @@ export class AdminClient extends DriftClient { swapFeeMin?: BN; swapFeeMax?: BN; oracleStalenessThreshold?: BN; - beta?: number; costToTradeBps?: number; + stablecoinWeight?: BN; } ): Promise { const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); return [ this.program.instruction.updateConstituentParams( - lpPoolName, Object.assign( { maxWeightDeviation: null, swapFeeMin: null, swapFeeMax: null, oracleStalenessThreshold: null, - beta: null, costToTradeBps: null, + stablecoinWeight: null, }, updateConstituentParams ), @@ -4634,7 +4640,6 @@ export class AdminClient extends DriftClient { ); return [ this.program.instruction.addAmmConstituentMappingData( - lpPoolName, addAmmConstituentMappingData, { accounts: { @@ -4675,7 +4680,6 @@ export class AdminClient extends DriftClient { ); return [ this.program.instruction.updateAmmConstituentMappingData( - lpPoolName, addAmmConstituentMappingData, { accounts: { @@ -4718,7 +4722,6 @@ export class AdminClient extends DriftClient { return [ this.program.instruction.removeAmmConstituentMappingData( - lpPoolName, perpMarketIndex, constituentIndex, { @@ -4733,4 +4736,286 @@ export class AdminClient extends DriftClient { ), ]; } + + /** + * Get the drift begin_swap and end_swap instructions + * + * @param outMarketIndex the market index of the token you're buying + * @param inMarketIndex the market index of the token you're selling + * @param amountIn the amount of the token to sell + * @param inTokenAccount the token account to move the tokens being sold (admin signer ata for lp swap) + * @param outTokenAccount the token account to receive the tokens being bought (admin signer ata for lp swap) + * @param limitPrice the limit price of the swap + * @param reduceOnly + * @param userAccountPublicKey optional, specify a custom userAccountPublicKey to use instead of getting the current user account; can be helpful if the account is being created within the current tx + */ + public async getSwapIx( + { + lpPoolName, + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }: { + lpPoolName: number[]; + outMarketIndex: number; + inMarketIndex: number; + amountIn: BN; + inTokenAccount: PublicKey; + outTokenAccount: PublicKey; + limitPrice?: BN; + reduceOnly?: SwapReduceOnly; + userAccountPublicKey?: PublicKey; + }, + lpSwap?: boolean + ): Promise<{ + beginSwapIx: TransactionInstruction; + endSwapIx: TransactionInstruction; + }> { + if (!lpSwap) { + return super.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }); + } + const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); + const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); + + const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); + + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const outConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const beginSwapIx = this.program.instruction.beginLpSwap( + inMarketIndex, + outMarketIndex, + amountIn, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + tokenProgram: inTokenProgram, + driftSigner: getDriftSignerPublicKey(this.program.programId), + }, + remainingAccounts: [ + { + pubkey: inSpotMarket.mint, + isWritable: false, + isSigner: false, + }, + ], + } + ); + + const remainingAccounts = []; + remainingAccounts.push({ + pubkey: outTokenProgram, + isWritable: false, + isSigner: false, + }); + remainingAccounts.push({ + pubkey: inSpotMarket.mint, + isWritable: false, + isSigner: false, + }); + remainingAccounts.push({ + pubkey: outSpotMarket.mint, + isWritable: false, + isSigner: false, + }); + + const endSwapIx = this.program.instruction.endLpSwap( + inMarketIndex, + outMarketIndex, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + tokenProgram: inTokenProgram, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + driftSigner: getDriftSignerPublicKey(this.program.programId), + }, + remainingAccounts, + } + ); + + return { beginSwapIx, endSwapIx }; + } + + public async getLpJupiterSwapIxV6({ + jupiterClient, + outMarketIndex, + inMarketIndex, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + quote, + lpPoolName, + }: { + jupiterClient: JupiterClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: QuoteResponse; + lpPoolName: number[]; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + if (!quote) { + const fetchedQuote = await jupiterClient.getQuote({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + + quote = fetchedQuote; + } + + if (!quote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + const isExactOut = swapMode === 'ExactOut' || quote.swapMode === 'ExactOut'; + const amountIn = new BN(quote.inAmount); + const exactOutBufferedAmountIn = amountIn.muln(1001).divn(1000); // Add 10bp buffer + + const transaction = await jupiterClient.getSwap({ + quote, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + const jupiterInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint: inMarket.mint, + outputMint: outMarket.mint, + }); + + const preInstructions = []; + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + const outAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const outAccountInfo = await this.connection.getAccountInfo( + outAssociatedTokenAccount + ); + if (!outAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + outAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + + const inTokenProgram = this.getTokenProgramForSpotMarket(inMarket); + const inAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + inTokenProgram + ); + + const inAccountInfo = await this.connection.getAccountInfo( + inAssociatedTokenAccount + ); + if (!inAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + inAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + lpPoolName, + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amountIn, + inTokenAccount: inAssociatedTokenAccount, + outTokenAccount: outAssociatedTokenAccount, + }); + + const ixs = [ + ...preInstructions, + beginSwapIx, + ...jupiterInstructions, + endSwapIx, + ]; + + return { ixs, lookupTables }; + } } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 305157ffff..46c1387025 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -58,7 +58,6 @@ import { UserStatsAccount, ProtectedMakerModeConfig, SignedMsgOrderParamsDelegateMessage, - AmmConstituentMapping, LPPoolAccount, } from './types'; import driftIDL from './idl/drift.json'; @@ -2447,17 +2446,19 @@ export class DriftClient { public async getAssociatedTokenAccount( marketIndex: number, useNative = true, - tokenProgram = TOKEN_PROGRAM_ID + tokenProgram = TOKEN_PROGRAM_ID, + authority = this.wallet.publicKey, + allowOwnerOffCurve = false ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); if (useNative && spotMarket.mint.equals(WRAPPED_SOL_MINT)) { - return this.wallet.publicKey; + return authority; } const mint = spotMarket.mint; return await getAssociatedTokenAddress( mint, - this.wallet.publicKey, - undefined, + authority, + allowOwnerOffCurve, tokenProgram ); } @@ -9714,7 +9715,6 @@ export class DriftClient { public async updateLpConstituentTargetBase( lpPoolName: number[], constituentIndexesToUpdate: number[], - ammConstituentMapping: AmmConstituentMapping, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( @@ -9748,7 +9748,6 @@ export class DriftClient { const ammCache = getAmmCachePublicKey(this.program.programId); return this.program.instruction.updateLpConstituentTargetBase( - lpPoolName, constituentIndexesToUpdate, { accounts: { @@ -9800,11 +9799,15 @@ export class DriftClient { }; }) ); - return this.program.instruction.updateLpPoolAum(lpPool.name, { + return this.program.instruction.updateLpPoolAum({ accounts: { keeper: this.wallet.publicKey, lpPool: lpPool.pubkey, state: await this.getStatePublicKey(), + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ), }, remainingAccounts, }); @@ -9938,7 +9941,6 @@ export class DriftClient { } public async lpPoolAddLiquidity({ - lpPoolName, inMarketIndex, inAmount, minMintAmount, @@ -9952,7 +9954,6 @@ export class DriftClient { inConstituent, txParams, }: { - lpPoolName: number[]; inMarketIndex: number; inAmount: BN; minMintAmount: BN; @@ -9969,7 +9970,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getLpPoolAddLiquidityIx({ - lpPoolName, inMarketIndex, inAmount, minMintAmount, @@ -9991,7 +9991,6 @@ export class DriftClient { } public async getLpPoolAddLiquidityIx({ - lpPoolName, inMarketIndex, inAmount, minMintAmount, @@ -10004,7 +10003,6 @@ export class DriftClient { inMarketMint, inConstituent, }: { - lpPoolName: number[]; inMarketIndex: number; inAmount: BN; minMintAmount: BN; @@ -10023,7 +10021,6 @@ export class DriftClient { }); return this.program.instruction.lpPoolAddLiquidity( - lpPoolName, inMarketIndex, inAmount, minMintAmount, @@ -10052,7 +10049,6 @@ export class DriftClient { } public async lpPoolRemoveLiquidity({ - lpPoolName, outMarketIndex, lpToBurn, minAmountOut, @@ -10066,7 +10062,6 @@ export class DriftClient { outConstituent, txParams, }: { - lpPoolName: number[]; outMarketIndex: number; lpToBurn: BN; minAmountOut: BN; @@ -10083,7 +10078,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction( await this.buildTransaction( await this.getLpPoolRemoveLiquidityIx({ - lpPoolName, outMarketIndex, lpToBurn, minAmountOut, @@ -10105,7 +10099,6 @@ export class DriftClient { } public async getLpPoolRemoveLiquidityIx({ - lpPoolName, outMarketIndex, lpToBurn, minAmountOut, @@ -10118,7 +10111,6 @@ export class DriftClient { outMarketMint, outConstituent, }: { - lpPoolName: number[]; outMarketIndex: number; lpToBurn: BN; minAmountOut: BN; @@ -10137,7 +10129,6 @@ export class DriftClient { }); return this.program.instruction.lpPoolRemoveLiquidity( - lpPoolName, outMarketIndex, lpToBurn, minAmountOut, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index f5f710a1a4..be09636cbb 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7356,15 +7356,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "spotMarketIndex", "type": "u16" @@ -7390,12 +7381,12 @@ "type": "u64" }, { - "name": "beta", + "name": "costToTrade", "type": "i32" }, { - "name": "costToTrade", - "type": "i32" + "name": "stablecoinWeight", + "type": "i64" } ] }, @@ -7404,7 +7395,7 @@ "accounts": [ { "name": "lpPool", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -7429,15 +7420,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "constituentParams", "type": { @@ -7481,15 +7463,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "ammConstituentMappingData", "type": { @@ -7530,15 +7503,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "ammConstituentMappingData", "type": { @@ -7579,15 +7543,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "perpMarketIndex", "type": "u16" @@ -7633,15 +7588,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "constituentIndexes", "type": { @@ -7667,19 +7613,14 @@ "name": "lpPool", "isMut": true, "isSigner": false - } - ], - "args": [ + }, { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false } - ] + ], + "args": [] }, { "name": "updateAmmCache", @@ -7860,15 +7801,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "inMarketIndex", "type": "u16" @@ -7953,15 +7885,6 @@ } ], "args": [ - { - "name": "lpPoolName", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "inMarketIndex", "type": "u16" @@ -7975,6 +7898,184 @@ "type": "u64" } ] + }, + { + "name": "beginLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + } + ] } ], "accounts": [ @@ -8552,13 +8653,16 @@ "type": "u64" }, { - "name": "padding2", - "type": { - "array": [ - "u8", - 8 - ] - } + "name": "stablecoinWeight", + "docs": [ + "percentable of stablecoin weight to go to this specific stablecoin PERCENTAGE_PRECISION", + "ZERO for non-stablecoins" + ], + "type": "i64" + }, + { + "name": "flashLoanInitialTokenAmount", + "type": "u64" } ] } @@ -10320,15 +10424,15 @@ } }, { - "name": "beta", + "name": "costToTradeBps", "type": { "option": "i32" } }, { - "name": "costToTradeBps", + "name": "stablecoinWeight", "type": { - "option": "i32" + "option": "i64" } } ] @@ -10710,20 +10814,25 @@ "kind": "struct", "fields": [ { - "name": "lastSlot", - "type": "u64" + "name": "costToTradeBps", + "type": "i32" }, { - "name": "targetBase", - "type": "i64" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "beta", - "type": "i32" + "name": "lastSlot", + "type": "u64" }, { - "name": "costToTradeBps", - "type": "i32" + "name": "targetBase", + "type": "i64" } ] } diff --git a/tests/lpPool.ts b/tests/lpPool.ts index e492e0df77..1bc4877063 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -255,7 +255,7 @@ describe('LP Pool', () => { new BN(2).mul(PERCENTAGE_PRECISION), new BN(400), 1, - 1 + PERCENTAGE_PRECISION ); const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( program.programId, @@ -327,7 +327,6 @@ describe('LP Pool', () => { encodeName(lpPoolName), constituentPublicKey, { - beta: 2, costToTradeBps: 10, } ); @@ -341,7 +340,6 @@ describe('LP Pool', () => { )) as ConstituentTargetBase; expect(targets).to.not.be.null; console.log(targets.targets[constituent.constituentIndex]); - assert(targets.targets[constituent.constituentIndex].beta == 2); assert(targets.targets[constituent.constituentIndex].costToTradeBps == 10); }); @@ -416,21 +414,9 @@ describe('LP Pool', () => { data: lpbuf, }); - const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( - program.programId, - lpPoolKey - ); - - const ammMapping = - (await adminClient.program.account.ammConstituentMapping.fetch( - ammConstituentMappingPublicKey - )) as AmmConstituentMapping; - - await adminClient.updateLpConstituentTargetBase( - encodeName(lpPoolName), - [0], - ammMapping - ); + await adminClient.updateLpConstituentTargetBase(encodeName(lpPoolName), [ + 0, + ]); const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( program.programId, lpPoolKey @@ -461,7 +447,7 @@ describe('LP Pool', () => { new BN(2).mul(PERCENTAGE_PRECISION), new BN(400), 1, - 1 + ZERO ); try { diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index f848d85e11..326d39cfee 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -1,7 +1,13 @@ import * as anchor from '@coral-xyz/anchor'; import { expect, assert } from 'chai'; import { Program } from '@coral-xyz/anchor'; -import { Keypair, PublicKey } from '@solana/web3.js'; +import { + Account, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; import { BN, TestClient, @@ -22,6 +28,8 @@ import { getConstituentPublicKey, ConstituentAccount, ZERO, + getSerumSignerPublicKey, + BN_MAX, } from '../sdk/src'; import { initializeQuoteSpotMarket, @@ -33,11 +41,17 @@ import { overwriteConstituentAccount, mockAtaTokenAccountForMint, overWriteMintAccount, + createWSolTokenAccountForUser, + initializeSolSpotMarket, + createUserWithUSDCAndWSOLAccount, } from './testHelpers'; import { startAnchor } from 'solana-bankrun'; import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; import dotenv from 'dotenv'; +import { DexInstructions, Market, OpenOrders } from '@project-serum/serum'; +import { listMarket, SERUM, makePlaceOrderTransaction } from './serumHelper'; +import { NATIVE_MINT } from '@solana/spl-token'; dotenv.config(); describe('LP Pool', () => { @@ -50,6 +64,18 @@ describe('LP Pool', () => { let spotTokenMint: Keypair; let spotMarketOracle: PublicKey; + let serumMarketPublicKey: PublicKey; + + let serumDriftClient: TestClient; + let serumWSOL: PublicKey; + let serumUSDC: PublicKey; + let serumKeypair: Keypair; + + let openOrdersAccount: PublicKey; + + const usdcAmount = new BN(500 * 10 ** 6); + const solAmount = new BN(2 * 10 ** 9); + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); const ammInitialQuoteAssetReserve = new anchor.BN(10 * 10 ** 13).mul( mantissaSqrtScale @@ -65,8 +91,22 @@ describe('LP Pool', () => { encodeName(lpPoolName) ); + let userUSDCAccount: Keypair; + let serumMarket: Market; + before(async () => { - const context = await startAnchor('', [], []); + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [] + ); // @ts-ignore bankrunContextWrapper = new BankrunContextWrapper(context); @@ -82,7 +122,7 @@ describe('LP Pool', () => { spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200.1); const keypair = new Keypair(); - await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + await bankrunContextWrapper.fundKeypair(keypair, 50 * LAMPORTS_PER_SOL); adminClient = new TestClient({ connection: bankrunContextWrapper.connection.toConnection(), @@ -94,7 +134,7 @@ describe('LP Pool', () => { activeSubAccountId: 0, subAccountIds: [], perpMarketIndexes: [0, 1], - spotMarketIndexes: [0, 1], + spotMarketIndexes: [0, 1, 2], oracleInfos: [ { publicKey: spotMarketOracle, @@ -110,7 +150,7 @@ describe('LP Pool', () => { await adminClient.subscribe(); await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); - const userUSDCAccount = await mockUserUSDCAccount( + userUSDCAccount = await mockUserUSDCAccount( usdcMint, new BN(10).mul(QUOTE_PRECISION), bankrunContextWrapper, @@ -184,7 +224,7 @@ describe('LP Pool', () => { PERCENTAGE_PRECISION.divn(100), // max 1% new BN(100), 1, - 1 + PERCENTAGE_PRECISION ); await adminClient.initializeConstituent( encodeName(lpPoolName), @@ -195,8 +235,40 @@ describe('LP Pool', () => { PERCENTAGE_PRECISION.divn(100), // max 1% new BN(100), 1, - 1 + ZERO + ); + + await initializeSolSpotMarket(adminClient, spotMarketOracle); + await adminClient.updateSpotMarketStepSizeAndTickSize( + 2, + new BN(100000000), + new BN(100) + ); + await adminClient.updateSpotAuctionDuration(0); + + [serumDriftClient, serumWSOL, serumUSDC, serumKeypair] = + await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + program, + solAmount, + usdcAmount, + [], + [0, 1], + [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await bankrunContextWrapper.fundKeypair( + serumKeypair, + 50 * LAMPORTS_PER_SOL ); + await serumDriftClient.deposit(usdcAmount, 0, serumUSDC); }); after(async () => { @@ -445,7 +517,6 @@ describe('LP Pool', () => { 0 ); await adminClient.lpPoolAddLiquidity({ - lpPoolName: encodeName(lpPoolName), inMarketIndex: 0, inAmount: tokensAdded, minMintAmount: new BN(1), @@ -490,7 +561,6 @@ describe('LP Pool', () => { // remove liquidity await adminClient.lpPoolRemoveLiquidity({ - lpPoolName: encodeName(lpPoolName), outMarketIndex: 0, lpToBurn: new BN(userLpTokenBalanceAfter.amount.toString()), minAmountOut: new BN(1), @@ -527,4 +597,221 @@ describe('LP Pool', () => { Number(totalC0TokensLost) / Number(tokensAdded); expect(totalC0TokensLostPercent).to.be.approximately(-0.013, 0.001); // lost about 1.3 swapping in an out }); + + it('Add Serum Market', async () => { + serumMarketPublicKey = await listMarket({ + context: bankrunContextWrapper, + wallet: bankrunContextWrapper.provider.wallet, + baseMint: NATIVE_MINT, + quoteMint: usdcMint.publicKey, + baseLotSize: 100000000, + quoteLotSize: 100, + dexProgramId: SERUM, + feeRateBps: 0, + }); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'confirmed' }, + SERUM + ); + + await adminClient.initializeSerumFulfillmentConfig( + 2, + serumMarketPublicKey, + SERUM + ); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const serumOpenOrdersAccount = new Account(); + const createOpenOrdersIx = await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + serumDriftClient.wallet.publicKey, + serumOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await serumDriftClient.sendTransaction( + new Transaction().add(createOpenOrdersIx), + [serumOpenOrdersAccount] + ); + + const adminOpenOrdersAccount = new Account(); + const adminCreateOpenOrdersIx = + await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + adminClient.wallet.publicKey, + adminOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await adminClient.sendTransaction( + new Transaction().add(adminCreateOpenOrdersIx), + [adminOpenOrdersAccount] + ); + + openOrdersAccount = adminOpenOrdersAccount.publicKey; + }); + + it('swap usdc for sol', async () => { + // Initialize new constituent for market 2 + await adminClient.initializeConstituent( + encodeName(lpPoolName), + 2, + 6, + PERCENTAGE_PRECISION.divn(10), // 10% max dev + PERCENTAGE_PRECISION.divn(10000), // min 1 bps + PERCENTAGE_PRECISION.divn(100), // max 1% + new BN(100), + 1, + ZERO + ); + + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + console.log(`beforeSOLBalance: ${beforeSOLBalance}`); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + console.log(`beforeUSDCBalance: ${beforeUSDCBalance}`); + + const serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const adminSolAccount = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + ZERO + ); + + // place ask to sell 1 sol for 100 usdc + const { transaction, signers } = await makePlaceOrderTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket, + { + owner: serumDriftClient.wallet, + payer: serumWSOL, + side: 'sell', + price: 100, + size: 1, + orderType: 'postOnly', + clientId: undefined, // todo? + openOrdersAddressKey: undefined, + openOrdersAccount: undefined, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + maxTs: BN_MAX, + } + ); + + const signerKeypairs = signers.map((signer) => { + return Keypair.fromSecretKey(signer.secretKey); + }); + + await serumDriftClient.sendTransaction(transaction, signerKeypairs); + + const amountIn = new BN(200).muln( + 10 ** adminClient.getSpotMarketAccount(0).decimals + ); + + const { beginSwapIx, endSwapIx } = await adminClient.getSwapIx( + { + lpPoolName: encodeName(lpPoolName), + amountIn: amountIn, + inMarketIndex: 0, + outMarketIndex: 2, + inTokenAccount: userUSDCAccount.publicKey, + outTokenAccount: adminSolAccount, + }, + true + ); + + const serumBidIx = serumMarket.makePlaceOrderInstruction( + bankrunContextWrapper.connection.toConnection(), + { + owner: adminClient.wallet.publicKey, + payer: userUSDCAccount.publicKey, + side: 'buy', + price: 100, + size: 2, // larger than maker orders so that entire maker order is taken + orderType: 'ioc', + clientId: new BN(1), // todo? + openOrdersAddressKey: openOrdersAccount, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + } + ); + + const serumConfig = await adminClient.getSerumV3FulfillmentConfig( + serumMarket.publicKey + ); + const settleFundsIx = DexInstructions.settleFunds({ + market: serumMarket.publicKey, + openOrders: openOrdersAccount, + owner: adminClient.wallet.publicKey, + // @ts-ignore + baseVault: serumConfig.serumBaseVault, + // @ts-ignore + quoteVault: serumConfig.serumQuoteVault, + baseWallet: adminSolAccount, + quoteWallet: userUSDCAccount.publicKey, + vaultSigner: getSerumSignerPublicKey( + serumMarket.programId, + serumMarket.publicKey, + serumConfig.serumSignerNonce + ), + programId: serumMarket.programId, + }); + + const tx = new Transaction() + .add(beginSwapIx) + .add(serumBidIx) + .add(settleFundsIx) + .add(endSwapIx); + + const { txSig } = await adminClient.sendTransaction(tx); + + bankrunContextWrapper.printTxLogs(txSig); + + // Balances should be accuarate after swap + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-100040000); + expect(solDiff).to.be.equal(1000000000); + }); }); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index 7b10cf3d1b..8ae78a5b22 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -16,7 +16,7 @@ import { getMintLen, ExtensionType, unpackAccount, - type RawAccount, + RawAccount, AccountState, unpackMint, RawMint,