diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 0b5a791fb0..f5d0824a90 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -20,7 +20,7 @@ drift-rs=[] [dependencies] anchor-lang = "0.29.0" solana-program = "1.16" -anchor-spl = "0.29.0" +anchor-spl = { version = "0.29.0", features = [] } pyth-client = "0.2.2" pyth-lazer-solana-contract = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "d790d1cb4da873a949cf33ff70349b7614b232eb", features = ["no-entrypoint"]} pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index e1fa1601a9..91d8f8bfec 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -8,7 +8,7 @@ use anchor_spl::token_2022::spl_token_2022::extension::{ }; use anchor_spl::token_2022::spl_token_2022::state::Mint as MintInner; use anchor_spl::token_interface::{ - self, CloseAccount, Mint, TokenAccount, TokenInterface, Transfer, TransferChecked, + self, Burn, CloseAccount, Mint, MintTo, TokenAccount, TokenInterface, Transfer, TransferChecked, }; pub fn send_from_program_vault<'info>( @@ -106,6 +106,58 @@ pub fn close_vault<'info>( token_interface::close_account(cpi_context) } +pub fn mint_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + nonce: u8, + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signature_seeds = get_signer_seeds(&nonce); + let signers = &[&signature_seeds[..]]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = MintTo { + mint: mint_account_info, + to: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::mint_to(cpi_context, amount) +} + +pub fn burn_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + nonce: u8, + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signature_seeds = get_signer_seeds(&nonce); + let signers = &[&signature_seeds[..]]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = Burn { + mint: mint_account_info, + from: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::burn(cpi_context, amount) +} + pub fn validate_mint_fee(account_info: &AccountInfo) -> Result<()> { let mint_data = account_info.try_borrow_data()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 61dee9f5f8..1d2335f6f8 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; - pub type DriftResult = std::result::Result; #[error_code] @@ -639,6 +638,34 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid Constituent")] + InvalidConstituent, + #[msg("Invalid Amm Constituent Mapping argument")] + InvalidAmmConstituentMappingArgument, + #[msg("Invalid update constituent update target weights argument")] + InvalidUpdateConstituentTargetBaseArgument, + #[msg("Constituent not found")] + ConstituentNotFound, + #[msg("Constituent could not load")] + ConstituentCouldNotLoad, + #[msg("Constituent wrong mutability")] + ConstituentWrongMutability, + #[msg("Wrong number of constituents passed to instruction")] + WrongNumberOfConstituents, + #[msg("Oracle too stale for LP AUM update")] + OracleTooStaleForLPAUMUpdate, + #[msg("Insufficient constituent token balance")] + InsufficientConstituentTokenBalance, + #[msg("Amm Cache data too stale")] + AMMCacheStale, + #[msg("LP Pool AUM not updated recently")] + LpPoolAumDelayed, + #[msg("Constituent oracle is stale")] + ConstituentOracleStale, + #[msg("LP Invariant failed")] + LpInvariantFailed, + #[msg("Invalid constituent derivative weights")] + InvalidConstituentDerivativeWeights, } #[macro_export] diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index e1964b8477..d958383ec0 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -84,3 +84,8 @@ pub mod lighthouse { use solana_program::declare_id; declare_id!("L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95"); } + +pub mod usdc_mint { + use solana_program::declare_id; + declare_id!("BJE5MMbqXjVwjAF7oxwPYXnTXDyspzZyt4vwenNw5ruG"); +} diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 47f7a92824..9f5aebda49 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,8 +1,16 @@ +use anchor_lang::Discriminator; +use anchor_spl::associated_token::AssociatedToken; use std::convert::identity; use std::mem::size_of; -use crate::math::amm::calculate_amm_available_liquidity; +use crate::math::amm::calculate_net_user_pnl; use crate::msg; +use crate::state::lp_pool::{ + AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, + ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, + CONSTITUENT_CORRELATIONS_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_BASE_PDA_SEED, + CONSTITUENT_VAULT_PDA_SEED, +}; use anchor_lang::prelude::*; use anchor_spl::token::Token; use anchor_spl::token_2022::Token2022; @@ -11,21 +19,25 @@ 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, usdc_mint, +}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO_I128, - DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, - 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, + AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, + FEE_POOL_TO_REVENUE_POOL_THRESHOLD, 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, 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::helpers::get_proportion_u128; @@ -58,13 +70,14 @@ use crate::state::oracle::{ use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::{InsuranceFundOperation, PerpOperation, SpotOperation}; use crate::state::perp_market::{ - ContractTier, ContractType, InsuranceClaim, MarketStatus, PerpMarket, PoolBalance, AMM, + AmmCache, CacheInfo, ContractTier, ContractType, InsuranceClaim, MarketStatus, PerpMarket, + PoolBalance, AMM, AMM_POSITIONS_CACHE, }; use crate::state::perp_market_map::{get_writable_perp_market_set, MarketSet}; use crate::state::protected_maker_mode_config::ProtectedMakerModeConfig; use crate::state::pyth_lazer_oracle::{PythLazerOracle, PYTH_LAZER_ORACLE_SEED}; use crate::state::spot_market::{ - AssetTier, InsuranceFund, SpotBalanceType, SpotFulfillmentConfigStatus, SpotMarket, + AssetTier, InsuranceFund, SpotBalance, SpotBalanceType, SpotFulfillmentConfigStatus, SpotMarket, }; use crate::state::spot_market_map::get_writable_spot_market_set; use crate::state::state::{ExchangeStatus, FeeStructure, OracleGuardRails, State}; @@ -82,6 +95,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); @@ -955,7 +970,9 @@ pub fn handle_initialize_perp_market( high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding: [0; 36], + lp_fee_transfer_scalar: 1, + lp_status: 0, + padding: [0; 34], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, @@ -1058,6 +1075,22 @@ pub fn handle_initialize_perp_market( safe_increment!(state.number_of_markets, 1); + let amm_cache = &mut ctx.accounts.amm_cache; + let current_len = amm_cache.cache.len(); + amm_cache + .cache + .resize_with(current_len + 1, CacheInfo::default); + let current_market_info = amm_cache.cache.get_mut(current_len).unwrap(); + current_market_info.slot = clock_slot; + + current_market_info.oracle = perp_market.amm.oracle; + current_market_info.oracle_source = u8::from(perp_market.amm.oracle_source); + current_market_info.last_oracle_price_twap = perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap; + amm_cache.validate(state)?; + controller::amm::update_concentration_coef(perp_market, concentration_coef_scale)?; crate::dlog!(oracle_price); @@ -1073,6 +1106,65 @@ pub fn handle_initialize_perp_market( Ok(()) } +pub fn handle_initialize_amm_cache(ctx: Context) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let state = &ctx.accounts.state; + amm_cache + .cache + .resize_with(state.number_of_markets as usize, CacheInfo::default); + amm_cache.bump = ctx.bumps.amm_cache; + + Ok(()) +} + +pub fn handle_update_init_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitAmmCacheInfo<'info>>, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + + let quote_market = spot_market_map.get_quote_spot_market()?; + + for (_, perp_market_loader) in perp_market_map.0 { + let perp_market = perp_market_loader.load()?; + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + + let market_index = perp_market.market_index as usize; + let cache = amm_cache.cache.get_mut(market_index).unwrap(); + cache.oracle = perp_market.amm.oracle; + cache.oracle_source = u8::from(perp_market.amm.oracle_source); + cache.last_oracle_price_twap = perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap; + cache.last_fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + "e_market, + perp_market.amm.fee_pool.balance_type(), + )?; + cache.last_net_pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + "e_market, + perp_market.pnl_pool.balance_type(), + )? + .cast::()? + .safe_sub(calculate_net_user_pnl(&perp_market.amm, oracle_data.price)?)?; + } + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -3227,10 +3319,11 @@ pub fn handle_update_perp_market_paused_operations( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_contract_tier( - ctx: Context, + ctx: Context, contract_tier: ContractTier, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); msg!( @@ -3240,6 +3333,14 @@ pub fn handle_update_perp_market_contract_tier( ); perp_market.contract_tier = contract_tier; + let max_confidence_interval_multiplier = + perp_market.get_max_confidence_interval_multiplier()?; + amm_cache + .cache + .get_mut(perp_market.market_index as usize) + .expect("value should exist for market index") + .max_confidence_interval_multiplier = max_confidence_interval_multiplier; + Ok(()) } @@ -3587,6 +3688,7 @@ pub fn handle_update_perp_market_oracle( skip_invariant_check: bool, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); let clock = Clock::get()?; @@ -3665,6 +3767,14 @@ pub fn handle_update_perp_market_oracle( perp_market.amm.oracle = oracle; perp_market.amm.oracle_source = oracle_source; + let amm_position_cache_info = amm_cache + .cache + .get_mut(perp_market.market_index as usize) + .expect("value should exist for market index"); + + amm_position_cache_info.oracle = oracle; + amm_position_cache_info.oracle_source = u8::from(oracle_source); + Ok(()) } @@ -3815,6 +3925,25 @@ pub fn handle_update_perp_market_min_order_size( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + lp_fee_transfer_scalar: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_fee_transfer_scalar, + lp_fee_transfer_scalar + ); + + perp_market.lp_fee_transfer_scalar = lp_fee_transfer_scalar; + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -4089,6 +4218,16 @@ pub fn handle_update_perp_market_protected_maker_params( Ok(()) } +pub fn handle_update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + perp_market.lp_status = lp_status; + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -4565,6 +4704,69 @@ pub fn handle_initialize_high_leverage_mode_config( Ok(()) } +pub fn handle_initialize_lp_pool( + ctx: Context, + name: [u8; 32], + min_mint_fee: i64, + max_mint_fee: i64, + revenue_rebalance_period: u64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_init()?; + let mint = ctx.accounts.mint.key(); + + *lp_pool = LPPool { + name, + pubkey: ctx.accounts.lp_pool.key(), + mint, + constituents: 0, + max_aum, + last_aum: 0, + last_aum_slot: 0, + last_aum_ts: 0, + max_settle_quote_amount: max_settle_quote_amount_per_market, + last_revenue_rebalance_ts: 0, + total_fees_received: 0, + total_fees_paid: 0, + total_mint_redeem_fees_paid: 0, + bump: ctx.bumps.lp_pool, + min_mint_fee, + max_mint_fee_premium: max_mint_fee, + revenue_rebalance_period, + next_mint_redeem_id: 1, + usdc_consituent_index: 0, + cumulative_usdc_sent_to_perp_markets: 0, + cumulative_usdc_received_from_perp_markets: 0, + gamma_execution: 2, + volatility: 4, + xi: 2, + }; + + let amm_constituent_mapping = &mut ctx.accounts.amm_constituent_mapping; + amm_constituent_mapping.lp_pool = ctx.accounts.lp_pool.key(); + amm_constituent_mapping.bump = ctx.bumps.amm_constituent_mapping; + amm_constituent_mapping + .weights + .resize_with(0 as usize, AmmConstituentDatum::default); + amm_constituent_mapping.validate()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + constituent_target_base.lp_pool = ctx.accounts.lp_pool.key(); + constituent_target_base.bump = ctx.bumps.constituent_target_base; + constituent_target_base + .targets + .resize_with(0 as usize, TargetsDatum::default); + constituent_target_base.validate()?; + + let consituent_correlations = &mut ctx.accounts.constituent_correlations; + consituent_correlations.lp_pool = ctx.accounts.lp_pool.key(); + consituent_correlations.bump = ctx.bumps.constituent_correlations; + consituent_correlations.correlations.resize(0 as usize, 0); + consituent_correlations.validate()?; + + Ok(()) +} pub fn handle_update_high_leverage_mode_config( ctx: Context, max_users: u32, @@ -4605,8 +4807,8 @@ pub fn handle_update_protected_maker_mode_config( ) -> Result<()> { let mut config = load_mut!(ctx.accounts.protected_maker_mode_config)?; - if current_users.is_some() { - config.current_users = current_users.unwrap(); + if let Some(users) = current_users { + config.current_users = users; } config.max_users = max_users; config.reduce_only = reduce_only as u8; @@ -4808,186 +5010,868 @@ pub fn handle_update_if_rebalance_config( Ok(()) } -#[derive(Accounts)] -pub struct Initialize<'info> { - #[account(mut)] - pub admin: Signer<'info>, - #[account( - init, - seeds = [b"drift_state".as_ref()], - space = State::SIZE, - bump, - payer = admin - )] - pub state: Box>, - pub quote_asset_mint: Box>, - /// CHECK: checked in `initialize` - pub drift_signer: AccountInfo<'info>, - pub rent: Sysvar<'info, Rent>, - pub system_program: Program<'info, System>, - pub token_program: Interface<'info, TokenInterface>, -} +pub fn handle_initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade_bps: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_init()?; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; -#[derive(Accounts)] -pub struct InitializeSpotMarket<'info> { - #[account( - init, - seeds = [b"spot_market", state.number_of_spot_markets.to_le_bytes().as_ref()], - space = SpotMarket::SIZE, - bump, - payer = admin - )] - pub spot_market: AccountLoader<'info, SpotMarket>, - pub spot_market_mint: Box>, - #[account( - init, - seeds = [b"spot_market_vault".as_ref(), state.number_of_spot_markets.to_le_bytes().as_ref()], - bump, - payer = admin, - token::mint = spot_market_mint, - token::authority = drift_signer - )] - pub spot_market_vault: Box>, - #[account( - init, - seeds = [b"insurance_fund_vault".as_ref(), state.number_of_spot_markets.to_le_bytes().as_ref()], - bump, - payer = admin, - token::mint = spot_market_mint, - token::authority = drift_signer - )] - pub insurance_fund_vault: Box>, - #[account( - constraint = state.signer.eq(&drift_signer.key()) - )] - /// CHECK: program signer - pub drift_signer: AccountInfo<'info>, - #[account(mut)] - pub state: Box>, - /// CHECK: checked in `initialize_spot_market` - pub oracle: AccountInfo<'info>, - #[account( - mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin - )] - pub admin: Signer<'info>, - pub rent: Sysvar<'info, Rent>, - pub system_program: Program<'info, System>, - pub token_program: Interface<'info, TokenInterface>, -} + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + let current_len = constituent_target_base.targets.len(); -#[derive(Accounts)] -#[instruction(market_index: u16)] -pub struct DeleteInitializedSpotMarket<'info> { - #[account(mut)] - pub admin: Signer<'info>, - #[account( - mut, - has_one = admin - )] - pub state: Box>, - #[account(mut, close = admin)] - pub spot_market: AccountLoader<'info, SpotMarket>, - #[account( - mut, - seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], - bump, - )] - pub spot_market_vault: Box>, - #[account( - mut, - seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], - bump, - )] - pub insurance_fund_vault: Box>, - /// CHECK: program signer - pub drift_signer: AccountInfo<'info>, - pub token_program: Interface<'info, TokenInterface>, -} + constituent_target_base + .targets + .resize_with((current_len + 1) as usize, TargetsDatum::default); -#[derive(Accounts)] -#[instruction(market_index: u16)] -pub struct InitializeSerumFulfillmentConfig<'info> { - #[account( - seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], - bump, - )] - pub base_spot_market: AccountLoader<'info, SpotMarket>, - #[account( - seeds = [b"spot_market", 0_u16.to_le_bytes().as_ref()], - bump, - )] - pub quote_spot_market: AccountLoader<'info, SpotMarket>, - #[account( - mut, - has_one = admin - )] - pub state: Box>, - /// CHECK: checked in ix - pub serum_program: AccountInfo<'info>, - /// CHECK: checked in ix - pub serum_market: AccountInfo<'info>, - #[account( - mut, - seeds = [b"serum_open_orders".as_ref(), serum_market.key.as_ref()], - bump, - )] - /// CHECK: checked in ix - pub serum_open_orders: AccountInfo<'info>, - #[account( - constraint = state.signer.eq(&drift_signer.key()) - )] - /// CHECK: program signer - pub drift_signer: AccountInfo<'info>, - #[account( - init, - seeds = [b"serum_fulfillment_config".as_ref(), serum_market.key.as_ref()], - space = SerumV3FulfillmentConfig::SIZE, - bump, - payer = admin, - )] - pub serum_fulfillment_config: AccountLoader<'info, SerumV3FulfillmentConfig>, - #[account(mut)] - pub admin: Signer<'info>, - pub rent: Sysvar<'info, Rent>, - pub system_program: Program<'info, System>, -} + let new_target = constituent_target_base + .targets + .get_mut(current_len) + .unwrap(); + new_target.cost_to_trade_bps = cost_to_trade_bps; + constituent_target_base.validate()?; -#[derive(Accounts)] -pub struct UpdateSerumFulfillmentConfig<'info> { - #[account( - has_one = admin - )] - pub state: Box>, - #[account(mut)] - pub serum_fulfillment_config: AccountLoader<'info, SerumV3FulfillmentConfig>, - #[account(mut)] - pub admin: Signer<'info>, + msg!( + "initializing constituent {} with spot market index {}", + lp_pool.constituents, + spot_market_index + ); + + validate!( + derivative_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; + constituent.max_weight_deviation = max_weight_deviation; + constituent.swap_fee_min = swap_fee_min; + constituent.swap_fee_max = swap_fee_max; + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + constituent.pubkey = ctx.accounts.constituent.key(); + constituent.mint = ctx.accounts.spot_market_mint.key(); + constituent.token_vault = ctx.accounts.constituent_vault.key(); + constituent.bump = ctx.bumps.constituent; + constituent.max_borrow_token_amount = max_borrow_token_amount; + constituent.lp_pool = lp_pool.pubkey; + constituent.constituent_index = (constituent_target_base.targets.len() - 1) as u16; + constituent.next_swap_id = 1; + constituent.constituent_derivative_index = constituent_derivative_index.unwrap_or(-1); + constituent.constituent_derivative_depeg_threshold = constituent_derivative_depeg_threshold; + constituent.derivative_weight = derivative_weight; + constituent.volatility = volatility; + constituent.gamma_execution = gamma_execution; + constituent.gamma_inventory = gamma_inventory; + constituent.xi = xi; + lp_pool.constituents += 1; + + if constituent.mint.eq(&usdc_mint::ID) { + lp_pool.usdc_consituent_index = constituent.constituent_index; + } + + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + validate!( + new_constituent_correlations.len() as u16 == lp_pool.constituents - 1, + ErrorCode::InvalidConstituent, + "expected {} correlations, got {}", + lp_pool.constituents, + new_constituent_correlations.len() + )?; + constituent_correlations.add_new_constituent(&new_constituent_correlations)?; + + Ok(()) } -#[derive(Accounts)] -#[instruction(market_index: u16)] -pub struct InitializePhoenixFulfillmentConfig<'info> { - #[account( - seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], - bump, - )] - pub base_spot_market: AccountLoader<'info, SpotMarket>, - #[account( - seeds = [b"spot_market", 0_u16.to_le_bytes().as_ref()], - bump, - )] - pub quote_spot_market: AccountLoader<'info, SpotMarket>, - #[account( - mut, - has_one = admin - )] - pub state: Box>, - /// CHECK: checked in ix - pub phoenix_program: AccountInfo<'info>, - /// CHECK: checked in ix - pub phoenix_market: AccountInfo<'info>, - #[account( +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConstituentParams { + pub max_weight_deviation: Option, + pub swap_fee_min: Option, + pub swap_fee_max: Option, + pub max_borrow_token_amount: Option, + pub oracle_staleness_threshold: Option, + pub cost_to_trade_bps: Option, + pub constituent_derivative_index: Option, + pub derivative_weight: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub gamma_inventory: Option, + pub xi: Option, +} + +pub fn handle_update_constituent_params<'info>( + ctx: Context, + constituent_params: ConstituentParams, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + if constituent_params.max_weight_deviation.is_some() { + msg!( + "max_weight_deviation: {:?} -> {:?}", + constituent.max_weight_deviation, + constituent_params.max_weight_deviation + ); + constituent.max_weight_deviation = constituent_params.max_weight_deviation.unwrap(); + } + + if constituent_params.swap_fee_min.is_some() { + msg!( + "swap_fee_min: {:?} -> {:?}", + constituent.swap_fee_min, + constituent_params.swap_fee_min + ); + constituent.swap_fee_min = constituent_params.swap_fee_min.unwrap(); + } + + if constituent_params.swap_fee_max.is_some() { + msg!( + "swap_fee_max: {:?} -> {:?}", + constituent.swap_fee_max, + constituent_params.swap_fee_max + ); + constituent.swap_fee_max = constituent_params.swap_fee_max.unwrap(); + } + + if constituent_params.oracle_staleness_threshold.is_some() { + msg!( + "oracle_staleness_threshold: {:?} -> {:?}", + constituent.oracle_staleness_threshold, + constituent_params.oracle_staleness_threshold + ); + constituent.oracle_staleness_threshold = + constituent_params.oracle_staleness_threshold.unwrap(); + } + + if constituent_params.cost_to_trade_bps.is_some() { + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + + let target = constituent_target_base + .targets + .get_mut(constituent.constituent_index as usize) + .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.derivative_weight.is_some() { + msg!( + "derivative_weight: {:?} -> {:?}", + constituent.derivative_weight, + constituent_params.derivative_weight + ); + constituent.derivative_weight = constituent_params.derivative_weight.unwrap(); + } + + if constituent_params.constituent_derivative_index.is_some() { + msg!( + "constituent_derivative_index: {:?} -> {:?}", + constituent.constituent_derivative_index, + constituent_params.constituent_derivative_index + ); + constituent.constituent_derivative_index = + constituent_params.constituent_derivative_index.unwrap(); + } + + if constituent_params.gamma_execution.is_some() { + msg!( + "gamma_execution: {:?} -> {:?}", + constituent.gamma_execution, + constituent_params.gamma_execution + ); + constituent.gamma_execution = constituent_params.gamma_execution.unwrap(); + } + + if constituent_params.gamma_inventory.is_some() { + msg!( + "gamma_inventory: {:?} -> {:?}", + constituent.gamma_inventory, + constituent_params.gamma_inventory + ); + constituent.gamma_inventory = constituent_params.gamma_inventory.unwrap(); + } + + if constituent_params.xi.is_some() { + msg!("xi: {:?} -> {:?}", constituent.xi, constituent_params.xi); + constituent.xi = constituent_params.xi.unwrap(); + } + + if let Some(max_borrow_token_amount) = constituent_params.max_borrow_token_amount { + msg!( + "max_borrow_token_amount: {:?} -> {:?}", + constituent.max_borrow_token_amount, + max_borrow_token_amount + ); + constituent.max_borrow_token_amount = max_borrow_token_amount; + } + + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LpPoolParams { + pub max_settle_quote_amount: Option, +} + +pub fn handle_update_lp_pool_params<'info>( + ctx: Context, + lp_pool_params: LpPoolParams, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + if let Some(max_settle_quote_amount) = lp_pool_params.max_settle_quote_amount { + msg!( + "max_settle_quote_amount: {:?} -> {:?}", + lp_pool.max_settle_quote_amount, + max_settle_quote_amount + ); + lp_pool.max_settle_quote_amount = max_settle_quote_amount; + } + + Ok(()) +} + +pub fn handle_update_amm_constituent_mapping_data<'info>( + ctx: Context, + amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + for datum in amm_constituent_mapping_data { + let existing_datum = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == datum.perp_market_index + && existing_datum.constituent_index == datum.constituent_index + }); + + if existing_datum.is_none() { + msg!( + "AmmConstituentDatum not found for perp_market_index {} and constituent_index {}", + datum.perp_market_index, + datum.constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights[existing_datum.unwrap()] = AmmConstituentDatum { + perp_market_index: datum.perp_market_index, + constituent_index: datum.constituent_index, + weight: datum.weight, + last_slot: Clock::get()?.slot, + ..AmmConstituentDatum::default() + }; + + msg!( + "Updated AmmConstituentDatum for perp_market_index {} and constituent_index {} to {}", + datum.perp_market_index, + datum.constituent_index, + datum.weight + ); + } + + Ok(()) +} + +pub fn handle_remove_amm_constituent_mapping_data<'info>( + ctx: Context, + perp_market_index: u16, + constituent_index: u16, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + let position = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == perp_market_index + && existing_datum.constituent_index == constituent_index + }); + + if position.is_none() { + msg!( + "Not found for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights.remove(position.unwrap()); + amm_mapping.weights.shrink_to_fit(); + + Ok(()) +} + +pub fn handle_add_amm_constituent_data<'info>( + ctx: Context, + init_amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + let constituent_target_base = &ctx.accounts.constituent_target_base; + let state = &ctx.accounts.state; + let mut current_len = amm_mapping.weights.len(); + + for init_datum in init_amm_constituent_mapping_data { + let perp_market_index = init_datum.perp_market_index; + + validate!( + perp_market_index < state.number_of_markets, + ErrorCode::InvalidAmmConstituentMappingArgument, + "perp_market_index too large compared to number of markets" + )?; + + validate!( + (init_datum.constituent_index as usize) < constituent_target_base.targets.len(), + ErrorCode::InvalidAmmConstituentMappingArgument, + "constituent_index too large compared to number of constituents in target weights" + )?; + + let constituent_index = init_datum.constituent_index; + let mut datum = AmmConstituentDatum::default(); + datum.perp_market_index = perp_market_index; + datum.constituent_index = constituent_index; + datum.weight = init_datum.weight; + datum.last_slot = Clock::get()?.slot; + + // Check if the datum already exists + let exists = amm_mapping.weights.iter().any(|d| { + d.perp_market_index == perp_market_index && d.constituent_index == constituent_index + }); + + validate!( + !exists, + ErrorCode::InvalidAmmConstituentMappingArgument, + "AmmConstituentDatum already exists for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + )?; + + // Add the new datum to the mapping + current_len += 1; + amm_mapping.weights.resize(current_len, datum); + } + + Ok(()) +} + +pub fn handle_update_constituent_correlation_data<'info>( + ctx: Context, + index1: u16, + index2: u16, + corr: i64, +) -> Result<()> { + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + constituent_correlations.set_correlation(index1, index2, corr)?; + + msg!( + "Updated correlation between constituent {} and {} to {}", + index1, + index2, + corr + ); + + constituent_correlations.validate()?; + + 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; + + 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)] + pub admin: Signer<'info>, + #[account( + init, + seeds = [b"drift_state".as_ref()], + space = State::SIZE, + bump, + payer = admin + )] + pub state: Box>, + pub quote_asset_mint: Box>, + /// CHECK: checked in `initialize` + pub drift_signer: AccountInfo<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct InitializeSpotMarket<'info> { + #[account( + init, + seeds = [b"spot_market", state.number_of_spot_markets.to_le_bytes().as_ref()], + space = SpotMarket::SIZE, + bump, + payer = admin + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + pub spot_market_mint: Box>, + #[account( + init, + seeds = [b"spot_market_vault".as_ref(), state.number_of_spot_markets.to_le_bytes().as_ref()], + bump, + payer = admin, + token::mint = spot_market_mint, + token::authority = drift_signer + )] + pub spot_market_vault: Box>, + #[account( + init, + seeds = [b"insurance_fund_vault".as_ref(), state.number_of_spot_markets.to_le_bytes().as_ref()], + bump, + payer = admin, + token::mint = spot_market_mint, + token::authority = drift_signer + )] + pub insurance_fund_vault: Box>, + #[account( + constraint = state.signer.eq(&drift_signer.key()) + )] + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub state: Box>, + /// CHECK: checked in `initialize_spot_market` + pub oracle: AccountInfo<'info>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction(market_index: u16)] +pub struct DeleteInitializedSpotMarket<'info> { + #[account(mut)] + pub admin: Signer<'info>, + #[account( + mut, + has_one = admin + )] + pub state: Box>, + #[account(mut, close = admin)] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction(market_index: u16)] +pub struct InitializeSerumFulfillmentConfig<'info> { + #[account( + seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], + bump, + )] + pub base_spot_market: AccountLoader<'info, SpotMarket>, + #[account( + seeds = [b"spot_market", 0_u16.to_le_bytes().as_ref()], + bump, + )] + pub quote_spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + has_one = admin + )] + pub state: Box>, + /// CHECK: checked in ix + pub serum_program: AccountInfo<'info>, + /// CHECK: checked in ix + pub serum_market: AccountInfo<'info>, + #[account( + mut, + seeds = [b"serum_open_orders".as_ref(), serum_market.key.as_ref()], + bump, + )] + /// CHECK: checked in ix + pub serum_open_orders: AccountInfo<'info>, + #[account( + constraint = state.signer.eq(&drift_signer.key()) + )] + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + #[account( + init, + seeds = [b"serum_fulfillment_config".as_ref(), serum_market.key.as_ref()], + space = SerumV3FulfillmentConfig::SIZE, + bump, + payer = admin, + )] + pub serum_fulfillment_config: AccountLoader<'info, SerumV3FulfillmentConfig>, + #[account(mut)] + pub admin: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct UpdateSerumFulfillmentConfig<'info> { + #[account( + has_one = admin + )] + pub state: Box>, + #[account(mut)] + pub serum_fulfillment_config: AccountLoader<'info, SerumV3FulfillmentConfig>, + #[account(mut)] + pub admin: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(market_index: u16)] +pub struct InitializePhoenixFulfillmentConfig<'info> { + #[account( + seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], + bump, + )] + pub base_spot_market: AccountLoader<'info, SpotMarket>, + #[account( + seeds = [b"spot_market", 0_u16.to_le_bytes().as_ref()], + bump, + )] + pub quote_spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + has_one = admin + )] + pub state: Box>, + /// CHECK: checked in ix + pub phoenix_program: AccountInfo<'info>, + /// CHECK: checked in ix + pub phoenix_market: AccountInfo<'info>, + #[account( constraint = state.signer.eq(&drift_signer.key()) )] /// CHECK: program signer @@ -5031,28 +5915,73 @@ pub struct UpdateSerumVault<'info> { } #[derive(Accounts)] -pub struct InitializePerpMarket<'info> { +pub struct InitializePerpMarket<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + #[account(mut)] + pub state: Box>, + #[account( + init, + seeds = [b"perp_market", state.number_of_markets.to_le_bytes().as_ref()], + space = PerpMarket::SIZE, + bump, + payer = admin + )] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(amm_cache.cache.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, + /// CHECK: checked in `initialize_perp_market` + pub oracle: AccountInfo<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct InitializeAmmCache<'info> { #[account( mut, constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin )] pub admin: Signer<'info>, - #[account(mut)] pub state: Box>, #[account( init, - seeds = [b"perp_market", state.number_of_markets.to_le_bytes().as_ref()], - space = PerpMarket::SIZE, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + space = AmmCache::space(state.number_of_markets as usize), bump, payer = admin )] - pub perp_market: AccountLoader<'info, PerpMarket>, - /// CHECK: checked in `initialize_perp_market` - pub oracle: AccountInfo<'info>, + pub amm_cache: Box>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct UpdateInitAmmCacheInfo<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub state: Box>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, +} + #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] @@ -5088,6 +6017,23 @@ pub struct HotAdminUpdatePerpMarket<'info> { pub perp_market: AccountLoader<'info, PerpMarket>, } +#[derive(Accounts)] +pub struct AdminUpdatePerpMarketContractTier<'info> { + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account(mut)] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, +} + #[derive(Accounts)] pub struct AdminUpdatePerpMarketAmmSummaryStats<'info> { #[account( @@ -5266,6 +6212,12 @@ pub struct AdminUpdatePerpMarketOracle<'info> { pub oracle: AccountInfo<'info>, /// CHECK: checked in `admin_update_perp_market_oracle` ix constraint pub old_oracle: AccountInfo<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, } #[derive(Accounts)] @@ -5614,3 +6566,337 @@ pub struct UpdateIfRebalanceConfig<'info> { )] pub state: Box>, } + +#[derive(Accounts)] +#[instruction( + name: [u8; 32], +)] +pub struct InitializeLpPool<'info> { + #[account(mut)] + pub admin: Signer<'info>, + #[account( + init, + seeds = [b"lp_pool", name.as_ref()], + space = LPPool::SIZE, + bump, + payer = admin + )] + pub lp_pool: AccountLoader<'info, LPPool>, + + pub mint: Account<'info, anchor_spl::token::Mint>, + + #[account( + init, + seeds = [b"LP_POOL_TOKEN_VAULT".as_ref(), lp_pool.key().as_ref()], + bump, + payer = admin, + token::mint = mint, + token::authority = drift_signer + )] + pub lp_pool_token_vault: Box>, + + #[account( + init, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = AmmConstituentMapping::space(0 as usize), + payer = admin, + )] + pub amm_constituent_mapping: Box>, + + #[account( + init, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentTargetBase::space(0 as usize), + payer = admin, + )] + pub constituent_target_base: Box>, + + #[account( + init, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentCorrelations::space(0 as usize), + payer = admin, + )] + pub constituent_correlations: Box>, + + #[account( + has_one = admin + )] + pub state: Box>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + + pub token_program: Program<'info, Token>, + + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + spot_market_index: u16, +)] +pub struct InitializeConstituent<'info> { + #[account()] + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + realloc = ConstituentCorrelations::space(constituent_target_base.targets.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_correlations: Box>, + + #[account( + init, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + space = Constituent::SIZE, + payer = admin, + )] + pub constituent: AccountLoader<'info, Constituent>, + pub spot_market_mint: Box>, + #[account( + init, + seeds = [CONSTITUENT_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + payer = admin, + token::mint = spot_market_mint, + token::authority = drift_signer + )] + pub constituent_vault: Box>, + #[account( + constraint = state.signer.eq(&drift_signer.key()) + )] + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentParams<'info> { + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + constraint = constituent.load()?.lp_pool == lp_pool.key() + )] + pub constituent_target_base: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateLpPoolParams<'info> { + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct AddAmmConstituentMappingDatum { + pub constituent_index: u16, + pub perp_market_index: u16, + pub weight: i64, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct AddAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() + amm_constituent_mapping_data.len()), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + pub state: Box>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct UpdateAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct RemoveAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() - 1), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentCorrelation<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + )] + pub constituent_correlations: Box>, + 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, + address = out_constituent.load()?.token_vault, + constraint = &out_constituent.load()?.mint.eq(&constituent_out_token_account.mint), + token::authority = drift_signer + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + address = in_constituent.load()?.token_vault, + 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/keeper.rs b/programs/drift/src/instructions/keeper.rs index 714c093991..f291b4ce60 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -4,6 +4,7 @@ use std::convert::TryFrom; use anchor_lang::prelude::*; use anchor_lang::Discriminator; use anchor_spl::associated_token::get_associated_token_address_with_program_id; +use anchor_spl::token_interface::Mint; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use solana_program::instruction::Instruction; use solana_program::pubkey; @@ -16,6 +17,7 @@ use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, }; use crate::controller::orders::cancel_orders; +use crate::controller::orders::validate_market_within_price_band; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; @@ -25,11 +27,15 @@ use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; +use crate::math::constants::QUOTE_PRECISION; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::constants::SPOT_BALANCE_PRECISION; +use crate::math::lp_pool::perp_lp_pool_settlement; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SignedMsgOrderRecord}; @@ -40,9 +46,15 @@ use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; use crate::state::fulfillment_params::serum::SerumFulfillmentParams; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::insurance_fund_stake::InsuranceFundStake; +use crate::state::lp_pool::Constituent; +use crate::state::lp_pool::LPPool; +use crate::state::lp_pool::CONSTITUENT_PDA_SEED; +use crate::state::lp_pool::SETTLE_AMM_ORACLE_MAX_DELAY; use crate::state::oracle_map::OracleMap; use crate::state::order_params::{OrderParams, PlaceOrderOptions}; use crate::state::paused_operations::{PerpOperation, SpotOperation}; +use crate::state::perp_market::CacheInfo; +use crate::state::perp_market::AMM_POSITIONS_CACHE; use crate::state::perp_market::{ContractType, MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ get_market_set_for_spot_positions, get_market_set_for_user_positions, get_market_set_from_list, @@ -54,6 +66,7 @@ use crate::state::signed_msg_user::{ SIGNED_MSG_PDA_SEED, }; use crate::state::spot_fulfillment_params::SpotFulfillmentParams; +use crate::state::spot_market::SpotBalance; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::{ get_writable_spot_market_set, get_writable_spot_market_set_from_many, SpotMarketMap, @@ -63,6 +76,8 @@ use crate::state::user::{ MarginMode, MarketType, OrderStatus, OrderTriggerCondition, OrderType, User, UserStats, }; use crate::state::user_map::{load_user_map, load_user_maps, UserMap, UserStatsMap}; +use crate::state::zero_copy::AccountZeroCopyMut; +use crate::state::zero_copy::ZeroCopyLoader; use crate::validation::sig_verification::verify_and_decode_ed25519_msg; use crate::validation::user::{validate_user_deletion, validate_user_is_idle}; use crate::{ @@ -2935,6 +2950,322 @@ pub fn handle_pause_spot_market_deposit_withdraw( Ok(()) } +// Refactored main function +pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, +) -> Result<()> { + use perp_lp_pool_settlement::*; + + let slot = Clock::get()?.slot; + let timestamp = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + + // Validation and setup code (unchanged) + let amm_cache_key = &ctx.accounts.amm_cache.key(); + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + let quote_market = &ctx.accounts.quote_market.load_mut()?; + let mut quote_constituent = ctx.accounts.constituent.load_mut()?; + let constituent_token_account = &mut ctx.accounts.constituent_quote_token_account; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + // PDA validation (unchanged) + let expected_pda = &Pubkey::create_program_address( + &[ + AMM_POSITIONS_CACHE.as_ref(), + amm_cache.fixed.bump.to_le_bytes().as_ref(), + ], + &crate::ID, + ) + .map_err(|_| ErrorCode::InvalidPDA)?; + validate!( + expected_pda.eq(amm_cache_key), + ErrorCode::InvalidPDA, + "Amm cache PDA does not match expected PDA" + )?; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map: _, + oracle_map: _, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + slot, + None, + )?; + + let precision_increase = SPOT_BALANCE_PRECISION.safe_div(QUOTE_PRECISION)?; + let mint = Some(*ctx.accounts.mint.clone()); + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let mut perp_market = perp_market_loader.load_mut()?; + if perp_market.lp_status == 0 { + continue; + } + + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + // Early validation checks (unchanged) + if slot.saturating_sub(cached_info.oracle_slot) > SETTLE_AMM_ORACLE_MAX_DELAY { + msg!( + "Skipping settling perp market {} to dlp because oracle slot is not up to date", + perp_market.market_index + ); + continue; + } + + validate_market_within_price_band(&perp_market, state, cached_info.oracle_price)?; + + if perp_market.is_operation_paused(PerpOperation::SettlePnl) { + msg!( + "Cannot settle pnl under current market = {} status", + perp_market.market_index + ); + continue; + } + + if cached_info.slot != slot { + msg!("Skipping settling perp market {} to lp pool because amm cache was not updated in the same slot", + perp_market.market_index); + return Err(ErrorCode::AMMCacheStale.into()); + } + + // Create settlement context + let settlement_ctx = SettlementContext { + quote_owed_from_lp: cached_info.quote_owed_from_lp_pool, + quote_constituent_token_balance: quote_constituent.token_balance, + fee_pool_balance: get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + pnl_pool_balance: get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + quote_market, + }; + + // Calculate settlement + let mut settlement_result = calculate_settlement_amount(&settlement_ctx)?; + + // If transfering from perp market, dont do more than the max allowed + if settlement_result.direction == SettlementDirection::ToLpPool { + settlement_result.amount_transferred = settlement_result + .amount_transferred + .min(lp_pool.max_settle_quote_amount); + } + + if settlement_result.direction == SettlementDirection::None { + continue; + } + + // Execute token transfer + match settlement_result.direction { + SettlementDirection::FromLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + constituent_token_account, + &ctx.accounts.quote_token_vault, + &ctx.accounts.drift_signer, + state.signer_nonce, + settlement_result.amount_transferred, + &mint, + )?; + } + SettlementDirection::ToLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.quote_token_vault, + constituent_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + settlement_result.amount_transferred, + &mint, + )?; + } + SettlementDirection::None => unreachable!(), + } + + // Update market pools + update_perp_market_pools(&mut perp_market, &settlement_result, precision_increase)?; + + // Calculate new quote owed amount + let new_quote_owed = match settlement_result.direction { + SettlementDirection::FromLpPool => cached_info + .quote_owed_from_lp_pool + .safe_sub(settlement_result.amount_transferred as i64)?, + SettlementDirection::ToLpPool => cached_info + .quote_owed_from_lp_pool + .safe_add(settlement_result.amount_transferred as i64)?, + SettlementDirection::None => cached_info.quote_owed_from_lp_pool, + }; + + // Update cache info + update_cache_info(cached_info, &settlement_result, new_quote_owed, timestamp)?; + + // Update LP pool stats + match settlement_result.direction { + SettlementDirection::FromLpPool => { + lp_pool.cumulative_usdc_sent_to_perp_markets = lp_pool + .cumulative_usdc_sent_to_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::ToLpPool => { + lp_pool.cumulative_usdc_received_from_perp_markets = lp_pool + .cumulative_usdc_received_from_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::None => {} + } + + // Sync constituent token balance + constituent_token_account.reload()?; + quote_constituent.sync_token_balance(constituent_token_account.amount); + } + + // Final validation + math::spot_withdraw::validate_spot_market_vault_amount( + quote_market, + ctx.accounts.quote_token_vault.amount, + )?; + + Ok(()) +} + +pub fn handle_update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, +) -> Result<()> { + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let amm_cache_key = &ctx.accounts.amm_cache.key(); + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let quote_market = ctx.accounts.quote_market.load()?; + + let expected_pda = &Pubkey::create_program_address( + &[ + AMM_POSITIONS_CACHE.as_ref(), + amm_cache.fixed.bump.to_le_bytes().as_ref(), + ], + &crate::ID, + ) + .map_err(|_| ErrorCode::InvalidPDA)?; + validate!( + expected_pda.eq(amm_cache_key), + ErrorCode::InvalidPDA, + "Amm cache PDA does not match expected PDA" + )?; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + let slot = Clock::get()?.slot; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let perp_market = perp_market_loader.load()?; + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + validate!( + perp_market.oracle_id() == cached_info.oracle_id()?, + ErrorCode::DefaultError, + "oracle id mismatch between amm cache and perp market" + )?; + + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + + cached_info.position = perp_market.amm.get_protocol_owned_position()?; + cached_info.slot = slot; + cached_info.last_oracle_price_twap = perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap; + cached_info.oracle_price = oracle_data.price; + cached_info.oracle_delay = oracle_data.delay; + cached_info.oracle_confidence = oracle_data.confidence; + cached_info.max_confidence_interval_multiplier = + perp_market.get_max_confidence_interval_multiplier()?; + + if perp_market.lp_status != 0 { + amm_cache.update_amount_owed_from_lp_pool(&perp_market, "e_market)?; + } + } + + Ok(()) +} + +#[derive(Accounts)] +pub struct SettleAmmPnlToLp<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + mut, + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + owner = crate::ID, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump = constituent.load()?.bump, + constraint = constituent.load()?.mint.eq("e_market.load()?.mint) + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.token_vault, + )] + pub constituent_quote_token_account: Box>, + #[account( + mut, + address = quote_market.load()?.vault, + token::authority = drift_signer, + )] + pub quote_token_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = quote_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateAmmCache<'info> { + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, +} + #[derive(Accounts)] pub struct FillOrder<'info> { pub state: Box>, diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs new file mode 100644 index 0000000000..d47c1a427e --- /dev/null +++ b/programs/drift/src/instructions/lp_pool.rs @@ -0,0 +1,1407 @@ +use anchor_lang::{prelude::*, Accounts, Key, Result}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::{ + controller::{ + self, + spot_balance::update_spot_balances, + token::{burn_tokens, mint_tokens}, + }, + error::ErrorCode, + get_then_update_id, + ids::admin_hot_wallet, + math::{ + self, + casting::Cast, + constants::{PERCENTAGE_PRECISION_I64, PRICE_PRECISION}, + oracle::{is_oracle_valid_for_action, oracle_validity, DriftAction}, + safe_math::SafeMath, + spot_balance, + }, + math_error, msg, safe_decrement, safe_increment, + state::{ + constituent_map::{ConstituentMap, ConstituentSet}, + events::{emit_stack, LPMintRedeemRecord, LPSwapRecord}, + lp_pool::{ + update_constituent_target_base_for_derivatives, AmmConstituentDatum, + AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, + ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, + MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC, + }, + oracle::OraclePriceData, + oracle_map::OracleMap, + perp_market::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE}, + perp_market_map::MarketSet, + spot_market::{SpotBalanceType, SpotMarket}, + spot_market_map::get_writable_spot_market_set_from_many, + state::State, + traits::Size, + user::MarketType, + zero_copy::{AccountZeroCopy, AccountZeroCopyMut, ZeroCopyLoader}, + }, + validate, +}; + +use solana_program::sysvar::clock::Clock; + +use super::optional_accounts::{load_maps, AccountMaps}; +use crate::controller::spot_balance::update_spot_market_cumulative_interest; +use crate::controller::token::{receive, send_from_program_vault}; +use crate::instructions::constraints::*; +use crate::state::lp_pool::{ + CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_BASE_PDA_SEED, LP_POOL_TOKEN_VAULT_PDA_SEED, +}; + +pub fn handle_update_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, +) -> Result<()> { + let slot = Clock::get()?.slot; + + let lp_pool_key: &Pubkey = &ctx.accounts.lp_pool.key(); + let amm_cache_key: &Pubkey = &ctx.accounts.amm_cache.key(); + + let amm_cache: AccountZeroCopy<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc()?; + + amm_cache.check_oracle_staleness(slot, MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC)?; + amm_cache.check_perp_market_staleness(slot, MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC)?; + + let expected_cache_pda = &Pubkey::create_program_address( + &[ + AMM_POSITIONS_CACHE.as_ref(), + amm_cache.fixed.bump.to_le_bytes().as_ref(), + ], + &crate::ID, + ) + .map_err(|_| ErrorCode::InvalidPDA)?; + validate!( + expected_cache_pda.eq(amm_cache_key), + ErrorCode::InvalidPDA, + "Amm cache PDA does not match expected PDA" + )?; + + let state = &ctx.accounts.state; + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(lp_pool_key), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let num_constituents = constituent_target_base.len(); + for datum in constituent_target_base.iter() { + msg!("weight datum: {:?}", datum); + } + + let slot = Clock::get()?.slot; + + let amm_constituent_mapping: AccountZeroCopy< + '_, + AmmConstituentDatum, + AmmConstituentMappingFixed, + > = ctx.accounts.amm_constituent_mapping.load_zc()?; + validate!( + amm_constituent_mapping.fixed.lp_pool.eq(lp_pool_key), + ErrorCode::InvalidPDA, + "Amm constituent mapping lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; + + let mut amm_inventories: Vec<(u16, i64, i64)> = + Vec::with_capacity(amm_constituent_mapping.len() as usize); + for (_, datum) in amm_constituent_mapping.iter().enumerate() { + let cache_info = amm_cache.get(datum.perp_market_index as u32); + + let oracle_validity = oracle_validity( + MarketType::Perp, + datum.perp_market_index, + cache_info.last_oracle_price_twap, + &OraclePriceData { + price: cache_info.oracle_price, + confidence: cache_info.oracle_confidence, + delay: cache_info.oracle_delay, + has_sufficient_number_of_data_points: true, + }, + &state.oracle_guard_rails.validity, + cache_info.max_confidence_interval_multiplier, + &cache_info.get_oracle_source()?, + true, + 0, + )?; + + if !is_oracle_valid_for_action( + oracle_validity, + Some(DriftAction::UpdateLpConstituentTargetBase), + )? { + msg!("Oracle data for perp market {} and constituent index {} is invalid. Skipping update", + datum.perp_market_index, datum.constituent_index); + continue; + } + + amm_inventories.push(( + datum.perp_market_index, + cache_info.position, + cache_info.oracle_price, + )); + } + + if amm_inventories.is_empty() { + msg!("No valid inventories found for constituent target weights update"); + return Ok(()); + } + + let mut constituent_indexes_and_decimals_and_prices: Vec<(u16, u8, i64)> = + Vec::with_capacity(constituent_map.0.len()); + for (index, loader) in &constituent_map.0 { + let constituent_ref = loader.load()?; + constituent_indexes_and_decimals_and_prices.push(( + *index, + constituent_ref.decimals, + constituent_ref.last_oracle_price, + )); + } + + let exists_invalid_constituent_index = constituent_indexes_and_decimals_and_prices + .iter() + .any(|(index, _, _)| *index as u32 >= num_constituents); + + validate!( + !exists_invalid_constituent_index, + ErrorCode::InvalidUpdateConstituentTargetBaseArgument, + "Constituent index larger than number of constituent target weights" + )?; + + constituent_target_base.update_target_base( + &amm_constituent_mapping, + amm_inventories.as_slice(), + constituent_indexes_and_decimals_and_prices.as_slice(), + slot, + )?; + + Ok(()) +} + +pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + let state = &ctx.accounts.state; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + oracle_map: _, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool.pubkey, remaining_accounts)?; + + validate!( + constituent_map.0.len() == lp_pool.constituents as usize, + ErrorCode::WrongNumberOfConstituents, + "Constituent map length does not match lp pool constituent count" + )?; + + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool.pubkey), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_cache_key: &Pubkey = &ctx.accounts.amm_cache.key(); + let amm_cache: AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc_mut()?; + let expected_amm_pda = &Pubkey::create_program_address( + &[ + AMM_POSITIONS_CACHE.as_ref(), + amm_cache.fixed.bump.to_le_bytes().as_ref(), + ], + &crate::ID, + ) + .map_err(|_| ErrorCode::InvalidPDA)?; + validate!( + amm_cache_key.eq(expected_amm_pda), + ErrorCode::InvalidPDA, + "Amm cache PDA does not match expected PDA" + )?; + + let (aum, crypto_delta, derivative_groups) = lp_pool.update_aum( + now, + slot, + &constituent_map, + &spot_market_map, + &constituent_target_base, + &amm_cache, + )?; + + // Set USDC stable weight + let total_stable_target_base = aum + .cast::()? + .safe_sub(crypto_delta.abs())? + .max(0_i128); + constituent_target_base + .get_mut(lp_pool.usdc_consituent_index as u32) + .target_base = total_stable_target_base.cast::()?; + + msg!( + "stable target base: {}", + constituent_target_base + .get(lp_pool.usdc_consituent_index as u32) + .target_base + ); + msg!("aum: {}, crypto_delta: {}", aum, crypto_delta); + msg!("derivative groups: {:?}", derivative_groups); + + update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + )?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, +) -> Result<()> { + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSpotMarketAccount, + "In and out spot market indices cannot be the same" + )?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + let lp_pool = &ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool.pubkey), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + validate!( + constituent_correlations.fixed.lp_pool.eq(&lp_pool.pubkey), + ErrorCode::InvalidPDA, + "Constituent correlations lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let in_target_weight = constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, + )?; + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, + )?; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + let out_amount_net_fees = if out_fee > 0 { + out_amount.safe_sub(out_fee.unsigned_abs())? + } else { + out_amount.safe_add(out_fee.unsigned_abs())? + }; + + validate!( + out_amount_net_fees.cast::()? >= min_out_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: out_amount_net_fees({}) < min_out_amount({})", + out_amount_net_fees, min_out_amount + ) + .as_str() + )?; + + validate!( + out_amount_net_fees.cast::()? <= out_constituent.token_balance, + ErrorCode::InsufficientConstituentTokenBalance, + format!( + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, out_constituent.token_balance + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee)?; + out_constituent.record_swap_fees(out_fee)?; + + let in_swap_id = get_then_update_id!(in_constituent, next_swap_id); + let out_swap_id = get_then_update_id!(out_constituent, next_swap_id); + + emit_stack::<_, { LPSwapRecord::SIZE }>(LPSwapRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + out_amount: out_amount_net_fees, + in_amount, + out_fee, + in_fee, + out_spot_market_index: out_market_index, + in_spot_market_index: in_market_index, + out_constituent_index: out_constituent.constituent_index, + in_constituent_index: in_constituent.constituent_index, + out_oracle_price: out_oracle.price, + in_oracle_price: in_oracle.price, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + out_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + out_market_target_weight: out_target_weight, + in_swap_id, + out_swap_id, + })?; + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + )?; + + send_from_program_vault( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + out_amount_net_fees.cast::()?, + &Some((*ctx.accounts.out_market_mint).clone()), + )?; + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.constituent_out_token_account.reload()?; + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![in_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + // TODO: check self.aum validity + + update_spot_market_cumulative_interest(&mut in_spot_market, Some(&in_oracle), now)?; + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + now, + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + let lp_mint_amount_net_fees = if lp_fee_amount > 0 { + lp_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + validate!( + lp_mint_amount_net_fees >= min_mint_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: lp_mint_amount_net_fees({}) < min_mint_amount({})", + lp_mint_amount_net_fees, min_mint_amount + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + msg!("receive"); + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + )?; + + msg!("mint_tokens"); + mint_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.drift_signer, + state.signer_nonce, + lp_amount, + &ctx.accounts.lp_mint, + )?; + + msg!("send_from_program_vault"); + send_from_program_vault( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + lp_mint_amount_net_fees, + &Some((*ctx.accounts.lp_mint).clone()), + )?; + + lp_pool.last_aum = lp_pool.last_aum.safe_add( + in_amount + .cast::()? + .safe_mul(in_oracle.price.cast::()?)? + .safe_div(10_u128.pow(in_spot_market.decimals))?, + )?; + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + let lp_price = if dlp_total_supply > 0 { + lp_pool + .last_aum + .safe_mul(PRICE_PRECISION)? + .safe_div(dlp_total_supply as u128)? + } else { + 0 + }; + + let mint_redeem_id = get_then_update_id!(lp_pool, next_mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 1, + amount: in_amount, + fee: in_fee_amount, + spot_market_index: in_market_index, + constituent_index: in_constituent.constituent_index, + oracle_price: in_oracle.price, + mint: in_constituent.mint, + lp_amount, + lp_fee: lp_fee_amount, + lp_price, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + })?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + out_market_index: u16, + lp_to_burn: u64, + min_amount_out: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + )?; + let out_oracle = out_oracle.clone(); + + // TODO: check self.aum validity + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + update_spot_market_cumulative_interest(&mut out_spot_market, Some(&out_oracle), now)?; + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + now, + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + let lp_burn_amount_net_fees = if lp_fee_amount > 0 { + lp_burn_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_burn_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + let out_amount_net_fees = if out_fee_amount > 0 { + out_amount.safe_sub(out_fee_amount.unsigned_abs())? + } else { + out_amount.safe_add(out_fee_amount.unsigned_abs())? + }; + let out_amount_net_fees = + out_amount_net_fees.min(ctx.accounts.constituent_out_token_account.amount as u128); + + validate!( + out_amount_net_fees >= min_amount_out, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: lp_mint_amount_net_fees({}) < min_mint_amount({})", + out_amount_net_fees, min_amount_out + ) + .as_str() + )?; + + out_constituent.record_swap_fees(out_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.authority, + lp_burn_amount, + &None, + )?; + + burn_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.drift_signer, + state.signer_nonce, + lp_burn_amount_net_fees, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + out_amount_net_fees.cast::()?, + &None, + )?; + + lp_pool.last_aum = lp_pool.last_aum.safe_sub( + out_amount_net_fees + .cast::()? + .safe_mul(out_oracle.price.cast::()?)? + .safe_div(10_u128.pow(out_spot_market.decimals))?, + )?; + + ctx.accounts.constituent_out_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + let lp_price = if dlp_total_supply > 0 { + lp_pool + .last_aum + .safe_mul(PRICE_PRECISION)? + .safe_div(dlp_total_supply as u128)? + } else { + 0 + }; + + let mint_redeem_id = get_then_update_id!(lp_pool, next_mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 0, + amount: out_amount, + fee: out_fee_amount, + spot_market_index: out_market_index, + constituent_index: out_constituent.constituent_index, + oracle_price: out_oracle.price, + mint: out_constituent.mint, + lp_amount: lp_burn_amount, + lp_fee: lp_fee_amount, + lp_price, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: out_target_weight, + })?; + + Ok(()) +} + +pub fn handle_update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, +) -> Result<()> { + let clock = Clock::get()?; + let mut constituent = ctx.accounts.constituent.load_mut()?; + let spot_market = ctx.accounts.spot_market.load()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + Ok(()) +} + +pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositWithdrawProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + let mut constituent = ctx.accounts.constituent.load_mut()?; + let spot_market_vault = &ctx.accounts.spot_market_vault; + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + let balance_before = constituent.get_full_balance(&spot_market)?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let deposit_plus_token_amount_before = amount.safe_add(spot_market_vault.amount)?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_data), + clock.unix_timestamp, + )?; + + controller::token::send_from_program_vault( + &ctx.accounts.token_program, + &ctx.accounts.constituent_token_account, + &spot_market_vault, + &ctx.accounts.drift_signer, + ctx.accounts.state.signer_nonce, + amount, + &Some(*ctx.accounts.mint.clone()), + )?; + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + spot_position, + false, + )?; + + safe_increment!(spot_position.cumulative_deposits, amount.cast()?); + + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.constituent_token_account.reload()?; + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + spot_market.validate_max_token_deposits_and_borrows(false)?; + + validate!( + ctx.accounts.spot_market_vault.amount == deposit_plus_token_amount_before, + ErrorCode::LpInvariantFailed, + "Spot market vault amount mismatch after deposit" + )?; + + validate!( + constituent + .get_full_balance(&spot_market)? + .abs_diff(balance_before) + <= 1, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after desposit to program vault" + )?; + + Ok(()) +} + +pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositWithdrawProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + let mut constituent = ctx.accounts.constituent.load_mut()?; + let spot_market_vault = &ctx.accounts.spot_market_vault; + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + + let balance_before = constituent.get_full_balance(&spot_market)?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_data), + clock.unix_timestamp, + )?; + + // Can only borrow up to the max + let token_amount = constituent.spot_balance.get_token_amount(&spot_market)?; + let amount_to_transfer = if constituent.spot_balance.balance_type == SpotBalanceType::Borrow { + amount.min( + constituent + .max_borrow_token_amount + .safe_sub(token_amount as u64)?, + ) + } else { + amount.min( + constituent + .max_borrow_token_amount + .safe_add(token_amount as u64)?, + ) + }; + + controller::token::send_from_program_vault( + &ctx.accounts.token_program, + &spot_market_vault, + &ctx.accounts.constituent_token_account, + &ctx.accounts.drift_signer, + state.signer_nonce, + amount_to_transfer, + &Some(*ctx.accounts.mint.clone()), + )?; + ctx.accounts.constituent_token_account.reload()?; + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount_to_transfer as u128, + &SpotBalanceType::Borrow, + &mut spot_market, + spot_position, + true, + )?; + + safe_decrement!( + spot_position.cumulative_deposits, + amount_to_transfer.cast()? + ); + + ctx.accounts.spot_market_vault.reload()?; + spot_market.validate_max_token_deposits_and_borrows(true)?; + + math::spot_withdraw::validate_spot_market_vault_amount( + &spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + + validate!( + constituent + .get_full_balance(&spot_market)? + .abs_diff(balance_before) + <= 1, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault" + )?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct DepositWithdrawProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.token_vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + token::authority = drift_signer + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + token::authority = drift_signer, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentOracleInfo<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentTargetBase<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmConstituentMappingZeroCopy checks + pub amm_constituent_mapping: AccountInfo<'info>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, +} + +#[derive(Accounts)] +pub struct UpdateLPPoolAum<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, +} + +/// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account +/// for the `in` token for this swap. +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPPoolSwap<'info> { + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.token_vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.token_vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + )] + pub user_in_token_account: Box>, + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + )] + pub user_out_token_account: Box>, + + #[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, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[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, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = in_market_mint.key() == in_constituent.load()?.mint, + )] + pub in_market_mint: Box>, + #[account( + constraint = out_market_mint.key() == out_constituent.load()?.mint, + )] + pub out_market_mint: Box>, + + pub authority: Signer<'info>, + + // TODO: in/out token program + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct LPPoolAddLiquidity<'info> { + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.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) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) + )] + pub user_in_token_account: Box>, + + #[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, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + #[account( + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct LPPoolRemoveLiquidity<'info> { + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + constraint = + out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) + )] + pub user_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, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + #[account( + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, +} diff --git a/programs/drift/src/instructions/mod.rs b/programs/drift/src/instructions/mod.rs index 0caa84f731..bc6c550c5f 100644 --- a/programs/drift/src/instructions/mod.rs +++ b/programs/drift/src/instructions/mod.rs @@ -2,6 +2,7 @@ pub use admin::*; pub use constraints::*; pub use if_staker::*; pub use keeper::*; +pub use lp_pool::*; pub use pyth_lazer_oracle::*; pub use pyth_pull_oracle::*; pub use user::*; @@ -10,6 +11,7 @@ mod admin; mod constraints; mod if_staker; mod keeper; +mod lp_pool; pub mod optional_accounts; mod pyth_lazer_oracle; mod pyth_pull_oracle; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 0e9674e008..431b83c77c 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -995,6 +995,18 @@ pub mod drift { ) } + pub fn initialize_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeAmmCache<'info>>, + ) -> Result<()> { + handle_initialize_amm_cache(ctx) + } + + pub fn update_init_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitAmmCacheInfo<'info>>, + ) -> Result<()> { + handle_update_init_amm_cache_info(ctx) + } + pub fn initialize_prediction_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AdminUpdatePerpMarket<'info>>, ) -> Result<()> { @@ -1046,6 +1058,13 @@ pub mod drift { handle_update_perp_market_expiry(ctx, expiry_ts) } + pub fn update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_status(ctx, lp_status) + } + pub fn settle_expired_market_pools_to_revenue_pool( ctx: Context, ) -> Result<()> { @@ -1322,7 +1341,7 @@ pub mod drift { } pub fn update_perp_market_contract_tier( - ctx: Context, + ctx: Context, contract_tier: ContractTier, ) -> Result<()> { handle_update_perp_market_contract_tier(ctx, contract_tier) @@ -1733,6 +1752,26 @@ pub mod drift { handle_initialize_high_leverage_mode_config(ctx, max_users) } + pub fn initialize_lp_pool( + ctx: Context, + name: [u8; 32], + min_mint_fee: i64, + max_mint_fee: i64, + revenue_rebalance_period: u64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + ) -> Result<()> { + handle_initialize_lp_pool( + ctx, + name, + min_mint_fee, + max_mint_fee, + revenue_rebalance_period, + max_aum, + max_settle_quote_amount_per_market, + ) + } + pub fn update_high_leverage_mode_config( ctx: Context, max_users: u32, @@ -1779,6 +1818,186 @@ pub mod drift { ) -> Result<()> { handle_update_if_rebalance_config(ctx, params) } + + pub fn initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, + ) -> Result<()> { + handle_initialize_constituent( + ctx, + spot_market_index, + decimals, + max_weight_deviation, + swap_fee_min, + swap_fee_max, + max_borrow_token_amount, + oracle_staleness_threshold, + cost_to_trade, + constituent_derivative_index, + constituent_derivative_depeg_threshold, + derivative_weight, + volatility, + gamma_execution, + gamma_inventory, + xi, + new_constituent_correlations, + ) + } + + pub fn update_constituent_params( + ctx: Context, + constituent_params: ConstituentParams, + ) -> Result<()> { + handle_update_constituent_params(ctx, constituent_params) + } + + pub fn update_lp_pool_params( + ctx: Context, + lp_pool_params: LpPoolParams, + ) -> Result<()> { + handle_update_lp_pool_params(ctx, lp_pool_params) + } + + pub fn add_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_add_amm_constituent_data(ctx, amm_constituent_mapping_data) + } + + pub fn update_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_update_amm_constituent_mapping_data(ctx, amm_constituent_mapping_data) + } + + pub fn remove_amm_constituent_mapping_data<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RemoveAmmConstituentMappingData<'info>>, + perp_market_index: u16, + constituent_index: u16, + ) -> Result<()> { + handle_remove_amm_constituent_mapping_data(ctx, perp_market_index, constituent_index) + } + + pub fn update_constituent_correlation_data( + ctx: Context, + index1: u16, + index2: u16, + correlation: i64, + ) -> Result<()> { + handle_update_constituent_correlation_data(ctx, index1, index2, correlation) + } + + pub fn update_lp_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, + ) -> Result<()> { + handle_update_constituent_target_base(ctx) + } + + pub fn update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, + ) -> Result<()> { + handle_update_lp_pool_aum(ctx) + } + + pub fn update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, + ) -> Result<()> { + handle_update_amm_cache(ctx) + } + + pub fn lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, + ) -> Result<()> { + handle_lp_pool_swap( + ctx, + in_market_index, + out_market_index, + in_amount, + min_out_amount, + ) + } + + pub fn lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, + ) -> Result<()> { + handle_lp_pool_add_liquidity(ctx, in_market_index, in_amount, min_mint_amount) + } + + pub fn lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + in_market_index: u16, + in_amount: u64, + min_out_amount: u128, + ) -> 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) + } + + pub fn update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, + ) -> Result<()> { + handle_update_constituent_oracle_info(ctx) + } + + pub fn deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositWithdrawProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_deposit_to_program_vault(ctx, amount) + } + + pub fn withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositWithdrawProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_withdraw_from_program_vault(ctx, amount) + } + + pub fn settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, + ) -> Result<()> { + handle_settle_perp_to_lp_pool(ctx) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 85a2944139..2c8b1b5321 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -53,6 +53,8 @@ pub const PERCENTAGE_PRECISION: u128 = 1_000_000; // expo -6 (represents 100%) pub const PERCENTAGE_PRECISION_I128: i128 = PERCENTAGE_PRECISION as i128; pub const PERCENTAGE_PRECISION_U64: u64 = PERCENTAGE_PRECISION as u64; pub const PERCENTAGE_PRECISION_I64: i64 = PERCENTAGE_PRECISION as i64; +pub const PERCENTAGE_PRECISION_I32: i32 = PERCENTAGE_PRECISION as i32; + pub const TEN_BPS: i128 = PERCENTAGE_PRECISION_I128 / 1000; pub const TEN_BPS_I64: i64 = TEN_BPS as i64; pub const TWO_PT_TWO_PCT: i128 = 22_000; diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs new file mode 100644 index 0000000000..a39aa71a98 --- /dev/null +++ b/programs/drift/src/math/lp_pool.rs @@ -0,0 +1,183 @@ +pub mod perp_lp_pool_settlement { + use crate::{ + math::safe_math::SafeMath, + state::{ + perp_market::{CacheInfo, PerpMarket}, + spot_market::{SpotBalance, SpotMarket}, + }, + *, + }; + use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + + #[derive(Debug, Clone, Copy)] + pub struct SettlementResult { + pub amount_transferred: u64, + pub direction: SettlementDirection, + pub fee_pool_used: u128, + pub pnl_pool_used: u128, + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub enum SettlementDirection { + ToLpPool, + FromLpPool, + None, + } + + pub struct SettlementContext<'a> { + pub quote_owed_from_lp: i64, + pub quote_constituent_token_balance: u64, + pub fee_pool_balance: u128, + pub pnl_pool_balance: u128, + pub quote_market: &'a SpotMarket, + } + + pub fn calculate_settlement_amount(ctx: &SettlementContext) -> Result { + if ctx.quote_owed_from_lp > 0 { + calculate_lp_to_perp_settlement(ctx) + } else if ctx.quote_owed_from_lp < 0 { + calculate_perp_to_lp_settlement(ctx) + } else { + Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + } + + fn calculate_lp_to_perp_settlement(ctx: &SettlementContext) -> Result { + if ctx.quote_constituent_token_balance == 0 { + return Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }); + } + + let amount_to_send = if ctx.quote_owed_from_lp > ctx.quote_constituent_token_balance as i64 + { + ctx.quote_constituent_token_balance + } else { + ctx.quote_owed_from_lp as u64 + }; + + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + + fn calculate_perp_to_lp_settlement(ctx: &SettlementContext) -> Result { + let amount_to_send = ctx.quote_owed_from_lp.abs() as u64; + + if ctx.fee_pool_balance >= amount_to_send as u128 { + // Fee pool can cover entire amount + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::ToLpPool, + fee_pool_used: amount_to_send as u128, + pnl_pool_used: 0, + }) + } else { + // Need to use both fee pool and pnl pool + let remaining_amount = (amount_to_send as u128).safe_sub(ctx.fee_pool_balance)?; + let pnl_pool_used = remaining_amount.min(ctx.pnl_pool_balance); + let actual_transfer = ctx.fee_pool_balance.safe_add(pnl_pool_used)?; + + Ok(SettlementResult { + amount_transferred: actual_transfer as u64, + direction: SettlementDirection::ToLpPool, + fee_pool_used: ctx.fee_pool_balance, + pnl_pool_used, + }) + } + } + + pub fn execute_token_transfer<'info>( + token_program: &Interface<'info, TokenInterface>, + from_vault: &InterfaceAccount<'info, TokenAccount>, + to_vault: &InterfaceAccount<'info, TokenAccount>, + signer: &AccountInfo<'info>, + signer_nonce: u8, + amount: u64, + mint: &Option>, + ) -> Result<()> { + controller::token::send_from_program_vault( + token_program, + from_vault, + to_vault, + signer, + signer_nonce, + amount, + mint, + ) + } + + // Market state updates + pub fn update_perp_market_pools( + perp_market: &mut PerpMarket, + result: &SettlementResult, + precision_increase: u128, + ) -> Result<()> { + match result.direction { + SettlementDirection::FromLpPool => { + perp_market.amm.fee_pool.increase_balance( + (result.amount_transferred as u128).safe_mul(precision_increase)?, + )?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + perp_market + .amm + .fee_pool + .decrease_balance(result.fee_pool_used.safe_mul(precision_increase)?)?; + } + if result.pnl_pool_used > 0 { + perp_market + .pnl_pool + .decrease_balance(result.pnl_pool_used.safe_mul(precision_increase)?)?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } + + pub fn update_cache_info( + cache_info: &mut CacheInfo, + result: &SettlementResult, + new_quote_owed: i64, + timestamp: i64, + ) -> Result<()> { + cache_info.quote_owed_from_lp_pool = new_quote_owed; + cache_info.last_settle_amount = result.amount_transferred; + cache_info.last_settle_ts = timestamp; + + match result.direction { + SettlementDirection::FromLpPool => { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_add(result.amount_transferred as u128)?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_sub(result.fee_pool_used)?; + } + if result.pnl_pool_used > 0 { + cache_info.last_net_pnl_pool_token_amount = cache_info + .last_net_pnl_pool_token_amount + .safe_sub(result.pnl_pool_used as i128)?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } +} diff --git a/programs/drift/src/math/mod.rs b/programs/drift/src/math/mod.rs index aa7ec7f196..d97eafcc6e 100644 --- a/programs/drift/src/math/mod.rs +++ b/programs/drift/src/math/mod.rs @@ -17,6 +17,7 @@ pub mod helpers; pub mod insurance; pub mod liquidation; pub mod lp; +pub mod lp_pool; pub mod margin; pub mod matching; pub mod oracle; diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 13d5da5fa0..84e2d5361b 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -69,6 +69,9 @@ pub enum DriftAction { UpdateTwap, UpdateAMMCurve, OracleOrderPrice, + UpdateLpConstituentTargetBase, + UpdateLpPoolAum, + LpPoolSwap, } pub fn is_oracle_valid_for_action( @@ -128,6 +131,15 @@ pub fn is_oracle_valid_for_action( ), DriftAction::UpdateTwap => !matches!(oracle_validity, OracleValidity::NonPositive), DriftAction::UpdateAMMCurve => !matches!(oracle_validity, OracleValidity::NonPositive), + DriftAction::UpdateLpConstituentTargetBase | DriftAction::UpdateLpPoolAum => { + !matches!(oracle_validity, OracleValidity::NonPositive) + } + DriftAction::LpPoolSwap => !matches!( + oracle_validity, + OracleValidity::NonPositive + | OracleValidity::StaleForAMM + | OracleValidity::InsufficientDataPoints + ), }, None => { matches!(oracle_validity, OracleValidity::Valid) @@ -224,7 +236,17 @@ pub fn oracle_validity( .. } = *oracle_price_data; - let is_oracle_price_nonpositive = oracle_price <= 0; + if oracle_price <= 0 { + // Return early so we dont panic with math errors later + if log_validity { + crate::msg!( + "Invalid {} {} Oracle: Non-positive (oracle_price <=0)", + market_type, + market_index + ); + } + return Ok(OracleValidity::NonPositive); + } let is_oracle_price_too_volatile = (oracle_price.max(last_oracle_twap)) .safe_div(last_oracle_twap.min(oracle_price).max(1))? @@ -256,9 +278,7 @@ pub fn oracle_validity( oracle_delay.gt(&valid_oracle_guard_rails.slots_before_stale_for_margin) }; - let oracle_validity = if is_oracle_price_nonpositive { - OracleValidity::NonPositive - } else if is_oracle_price_too_volatile { + let oracle_validity = if is_oracle_price_too_volatile { OracleValidity::TooVolatile } else if is_conf_too_large { OracleValidity::TooUncertain @@ -281,14 +301,6 @@ pub fn oracle_validity( ); } - if is_oracle_price_nonpositive { - crate::msg!( - "Invalid {} {} Oracle: Non-positive (oracle_price <=0)", - market_type, - market_index - ); - } - if is_oracle_price_too_volatile { crate::msg!( "Invalid {} {} Oracle: Too Volatile (last_oracle_price_twap={:?} vs oracle_price={:?})", diff --git a/programs/drift/src/state/constituent_map.rs b/programs/drift/src/state/constituent_map.rs new file mode 100644 index 0000000000..abc0469cc3 --- /dev/null +++ b/programs/drift/src/state/constituent_map.rs @@ -0,0 +1,246 @@ +use anchor_lang::accounts::account_loader::AccountLoader; +use std::cell::{Ref, RefMut}; +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::Peekable; +use std::slice::Iter; + +use anchor_lang::prelude::{AccountInfo, Pubkey}; + +use anchor_lang::Discriminator; +use arrayref::array_ref; + +use crate::error::{DriftResult, ErrorCode}; + +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::traits::Size; +use crate::{msg, validate}; +use std::panic::Location; + +use super::lp_pool::Constituent; + +pub struct ConstituentMap<'a>(pub BTreeMap>); + +impl<'a> ConstituentMap<'a> { + #[track_caller] + #[inline(always)] + pub fn get_ref(&self, constituent_index: &u16) -> DriftResult> { + let loader = match self.0.get(constituent_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load() { + Ok(constituent) => Ok(constituent), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_ref_mut(&self, market_index: &u16) -> DriftResult> { + let loader = match self.0.get(market_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load_mut() { + Ok(perp_market) => Ok(perp_market), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + pub fn load<'b, 'c>( + writable_constituents: &'b ConstituentSet, + lp_pool_key: &Pubkey, + account_info_iter: &'c mut Peekable>>, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + while let Some(account_info) = account_info_iter.peek() { + if account_info.owner != &crate::ID { + break; + } + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + msg!( + "didnt match constituent size, {}, {}", + data.len(), + expected_data_len + ); + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + msg!( + "didnt match account discriminator {:?}, {:?}", + account_discriminator, + constituent_discriminator + ); + break; + } + + // Pubkey + let constituent_lp_key = Pubkey::from(*array_ref![data, 72, 32]); + validate!( + &constituent_lp_key == lp_pool_key, + ErrorCode::InvalidConstituent, + "Constituent lp pool pubkey does not match lp pool pubkey" + )?; + + // constituent index 276 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 292, 2]); + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + let is_writable = account_info.is_writable; + if writable_constituents.contains(&constituent_index) && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } +} + +#[cfg(test)] +impl<'a> ConstituentMap<'a> { + pub fn load_one<'c: 'a>( + account_info: &'c AccountInfo<'a>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + // market index 1160 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 42, 2]); + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = + AccountLoader::try_from(account_info).or(Err(ErrorCode::InvalidMarketAccount))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + + Ok(constituent_map) + } + + pub fn load_multiple<'c: 'a>( + account_info: Vec<&'c AccountInfo<'a>>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let account_info_iter = account_info.into_iter(); + for account_info in account_info_iter { + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + // constituent index 284 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 284, 2]); + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } + + pub fn empty() -> Self { + ConstituentMap(BTreeMap::new()) + } +} + +pub(crate) type ConstituentSet = BTreeSet; diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 8f93fae37e..6e17644add 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -733,3 +733,86 @@ pub fn emit_buffers( Ok(()) } + +#[event] +#[derive(Default)] +pub struct LPSwapRecord { + pub ts: i64, + pub slot: u64, + pub authority: Pubkey, + /// precision: out market mint precision, gross fees + pub out_amount: u128, + /// precision: in market mint precision, gross fees + pub in_amount: u128, + /// precision: fee on amount_out, in market mint precision + pub out_fee: i128, + /// precision: fee on amount_in, out market mint precision + pub in_fee: i128, + // out spot market index + pub out_spot_market_index: u16, + // in spot market index + pub in_spot_market_index: u16, + // out constituent index + pub out_constituent_index: u16, + // in constituent index + pub in_constituent_index: u16, + /// precision: PRICE_PRECISION + pub out_oracle_price: i64, + /// precision: PRICE_PRECISION + pub in_oracle_price: i64, + /// LPPool last_aum, QUOTE_PRECISION + pub last_aum: u128, + pub last_aum_slot: u64, + /// PERCENTAGE_PRECISION + pub in_market_current_weight: i64, + /// PERCENTAGE_PRECISION + pub out_market_current_weight: i64, + /// PERCENTAGE_PRECISION + pub in_market_target_weight: i64, + /// PERCENTAGE_PRECISION + pub out_market_target_weight: i64, + pub in_swap_id: u64, + pub out_swap_id: u64, +} + +impl Size for LPSwapRecord { + const SIZE: usize = 376; +} + +#[event] +#[derive(Default)] +pub struct LPMintRedeemRecord { + pub ts: i64, + pub slot: u64, + pub authority: Pubkey, + pub description: u8, + /// precision: continutent mint precision, gross fees + pub amount: u128, + /// precision: fee on amount, constituent market mint precision + pub fee: i128, + // spot market index + pub spot_market_index: u16, + // constituent index + pub constituent_index: u16, + /// precision: PRICE_PRECISION + pub oracle_price: i64, + /// token mint + pub mint: Pubkey, + /// lp amount, lp mint precision + pub lp_amount: u64, + /// lp fee, lp mint precision + pub lp_fee: i64, + /// the fair price of the lp token, PRICE_PRECISION + pub lp_price: u128, + pub mint_redeem_id: u64, + /// LPPool last_aum + pub last_aum: u128, + pub last_aum_slot: u64, + /// PERCENTAGE_PRECISION + pub in_market_current_weight: i64, + pub in_market_target_weight: i64, +} + +impl Size for LPMintRedeemRecord { + const SIZE: usize = 328; +} diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs new file mode 100644 index 0000000000..374cf78d40 --- /dev/null +++ b/programs/drift/src/state/lp_pool.rs @@ -0,0 +1,1604 @@ +use std::collections::BTreeMap; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::constants::{ + BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, QUOTE_PRECISION_I128, +}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::constituent_map::ConstituentMap; +use crate::state::perp_market::{AmmCacheFixed, CacheInfo}; +use crate::state::spot_market_map::SpotMarketMap; +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::oracle::OraclePriceData; +use super::spot_market::SpotMarket; +use super::zero_copy::{AccountZeroCopy, AccountZeroCopyMut, HasLen}; +use crate::state::spot_market::{SpotBalance, SpotBalanceType}; +use crate::state::traits::Size; +use crate::{impl_zero_copy_loader, validate}; + +pub const AMM_MAP_PDA_SEED: &str = "AMM_MAP"; +pub const CONSTITUENT_PDA_SEED: &str = "CONSTITUENT"; +pub const CONSTITUENT_TARGET_BASE_PDA_SEED: &str = "constituent_target_base"; +pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; +pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; +pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; + +pub const BASE_SWAP_FEE: i128 = 300; // 0.75% in PERCENTAGE_PRECISION +pub const MAX_SWAP_FEE: i128 = 75_000; // 0.75% in PERCENTAGE_PRECISION +pub const MIN_SWAP_FEE: i128 = 200; // 0.75% in PERCENTAGE_PRECISION + +pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000; + +// Delay constants +#[cfg(feature = "anchor-test")] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 100; +#[cfg(not(feature = "anchor-test"))] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 10; +pub const LP_POOL_SWAP_AUM_UPDATE_DELAY: u64 = 0; +#[cfg(feature = "anchor-test")] +pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 0u64; + +#[cfg(test)] +mod tests; + +#[account(zero_copy(unsafe))] +#[derive(Default, Debug)] +#[repr(C)] +pub struct LPPool { + /// name of vault, TODO: check type + size + pub name: [u8; 32], // 32 + /// address of the vault. + pub pubkey: Pubkey, // 32, 64 + // vault token mint + pub mint: Pubkey, // 32, 96 + + /// The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index) + /// which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0) + /// pub quote_constituent_index: u16, + + /// QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this + pub max_aum: u128, // 8, 136 + + /// QUOTE_PRECISION: AUM of the vault in USD, updated lazily + pub last_aum: u128, // 8, 144 + + /// timestamp of last AUM slot + pub last_aum_slot: u64, // 8, 152 + /// timestamp of last AUM update + pub last_aum_ts: i64, // 8, 160 + + /// Oldest slot of constituent oracles + pub max_settle_quote_amount: u64, + + /// timestamp of last vAMM revenue rebalance + pub last_revenue_rebalance_ts: u64, // 8, 168 + pub revenue_rebalance_period: u64, + + /// Every mint/redeem has a monotonically increasing id. This is the next id to use + pub next_mint_redeem_id: u64, + + /// all revenue settles recieved + pub total_fees_received: u128, // 16, 176 + /// all revenues paid out + pub total_fees_paid: u128, // 16, 192 + + pub cumulative_usdc_sent_to_perp_markets: u128, + pub cumulative_usdc_received_from_perp_markets: u128, + + pub total_mint_redeem_fees_paid: i128, + + pub min_mint_fee: i64, + pub max_mint_fee_premium: i64, + + pub constituents: u16, // 2, 194 + + pub bump: u8, + + pub usdc_consituent_index: u16, + + pub gamma_execution: u8, + pub xi: u8, + pub volatility: u64, +} + +impl Size for LPPool { + const SIZE: usize = 296; +} + +impl LPPool { + pub fn get_price(&self, mint: &Mint) -> Result { + match mint.supply { + 0 => Ok(0), + supply => { + // TODO: assuming mint decimals = quote decimals = 6 + (supply as u128) + .checked_div(self.last_aum) + .ok_or(ErrorCode::MathError.into()) + } + } + } + + /// Get the swap price between two (non-LP token) constituents. + /// Accounts for precision differences between in and out constituents + /// returns swap price in PRICE_PRECISION + pub fn get_swap_price( + &self, + in_decimals: u32, + out_decimals: u32, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + ) -> DriftResult<(u64, u64)> { + let in_price = in_oracle.price.cast::()?; + let out_price = out_oracle.price.cast::()?; + + let (prec_diff_numerator, prec_diff_denominator) = if out_decimals > in_decimals { + (10_u64.pow(out_decimals - in_decimals), 1) + } else { + (1, 10_u64.pow(in_decimals - out_decimals)) + }; + + let swap_price_num = in_price.safe_mul(prec_diff_numerator)?; + let swap_price_denom = out_price.safe_mul(prec_diff_denominator)?; + + Ok((swap_price_num, swap_price_denom)) + } + + /// in the respective token units. Amounts are gross fees and in + /// token mint precision. + /// Positive fees are paid, negative fees are rebated + /// Returns (in_amount out_amount, in_fee, out_fee) + pub fn get_swap_amount( + &self, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + in_constituent: &Constituent, + out_constituent: &Constituent, + in_spot_market: &SpotMarket, + out_spot_market: &SpotMarket, + in_target_weight: i64, + out_target_weight: i64, + in_amount: u128, + correlation: i64, + ) -> DriftResult<(u128, u128, i128, i128)> { + let (swap_price_num, swap_price_denom) = self.get_swap_price( + in_spot_market.decimals, + out_spot_market.decimals, + in_oracle, + out_oracle, + )?; + + let (in_fee, out_fee) = self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + Some(out_spot_market), + Some(out_oracle.price), + Some(out_constituent), + Some(out_target_weight), + correlation, + )?; + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let out_amount = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .safe_mul(swap_price_num.cast::()?)? + .safe_div(swap_price_denom.cast::()?)? + .cast::()?; + + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((in_amount, out_amount, in_fee_amount, out_fee_amount)) + } + + /// Calculates the amount of LP tokens to mint for a given input of constituent tokens. + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_add_liquidity_mint_amount( + &self, + now: i64, + in_spot_market: &SpotMarket, + in_constituent: &Constituent, + in_amount: u128, + in_oracle: &OraclePriceData, + in_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let (in_fee_pct, out_fee_pct) = if self.last_aum == 0 { + (0, 0) + } else { + self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + None, + None, + None, + None, + 0, + )? + }; + let in_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let in_amount_less_fees = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .max(0) + .cast::()?; + + let token_precision_denominator = 10_u128.pow(in_spot_market.decimals); + let token_amount_usd = in_oracle + .price + .cast::()? + .safe_mul(in_amount_less_fees)?; + let lp_amount = if self.last_aum == 0 { + token_amount_usd.safe_div(token_precision_denominator)? + } else { + token_amount_usd + .safe_mul(dlp_total_supply.max(1) as u128)? + .safe_div(self.last_aum.safe_mul(token_precision_denominator)?)? + }; + + let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, true)?; + let lp_fee_to_charge = lp_amount + .safe_mul(lp_fee_to_charge_pct as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .cast::()?; + + Ok(( + lp_amount.cast::()?, + in_amount, + lp_fee_to_charge, + in_fee_amount, + )) + } + + /// Calculates the amount of constituent tokens to receive for a given amount of LP tokens to burn + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_remove_liquidity_amount( + &self, + now: i64, + out_spot_market: &SpotMarket, + out_constituent: &Constituent, + lp_burn_amount: u64, + out_oracle: &OraclePriceData, + out_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, false)?; + let lp_fee_to_charge = lp_burn_amount + .cast::()? + .safe_mul(lp_fee_to_charge_pct.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .cast::()?; + + let lp_amount_less_fees = (lp_burn_amount as i128).safe_sub(lp_fee_to_charge as i128)?; + + let token_precision_denominator = 10_u128.pow(out_spot_market.decimals); + + // Calculate proportion of LP tokens being burned + let proportion = lp_amount_less_fees + .cast::()? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(dlp_total_supply as u128)?; + + // Apply proportion to AUM and convert to token amount + let out_amount = self + .last_aum + .safe_mul(proportion)? + .safe_mul(token_precision_denominator)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(out_oracle.price.cast::()?)?; + + let (in_fee_pct, out_fee_pct) = self.get_swap_fees( + out_spot_market, + out_oracle.price, + out_constituent, + out_amount.cast::()?.safe_mul(-1_i128)?, + out_target_weight, + None, + None, + None, + None, + 0, + )?; + let out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + let out_fee_amount = out_amount + .safe_mul(out_fee_pct.cast::()?)? + .safe_div(PERCENTAGE_PRECISION)? + .cast::()?; + + Ok((lp_burn_amount, out_amount, lp_fee_to_charge, out_fee_amount)) + } + + pub fn get_quadratic_fee_inventory( + &self, + gamma_covar: [[i128; 2]; 2], + pre_notional_errors: [i128; 2], + post_notional_errors: [i128; 2], + trade_notional: i128, + ) -> DriftResult<(i128, i128)> { + let gamma_covar_error_pre_in = gamma_covar[0][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_pre_out = gamma_covar[1][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let gamma_covar_error_post_in = gamma_covar[0][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_post_out = gamma_covar[1][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let c_pre_in: i128 = gamma_covar_error_pre_in + .safe_mul(pre_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_pre_out = gamma_covar_error_pre_out + .safe_mul(pre_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let c_post_in: i128 = gamma_covar_error_post_in + .safe_mul(post_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_post_out = gamma_covar_error_post_out + .safe_mul(post_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let in_fee = c_post_in + .safe_sub(c_pre_in)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + let out_fee = c_post_out + .safe_sub(c_pre_out)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + + Ok((in_fee, out_fee)) + } + + pub fn get_linear_fee_execution( + &self, + trade_notional: i128, + kappa_execution: u64, + xi: u8, + spot_depth: u128, + ) -> DriftResult { + let trade_ratio: i128 = trade_notional + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(spot_depth.cast::()?)?; + + trade_ratio + .safe_mul(kappa_execution.safe_mul(xi as u64)?.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + pub fn get_quadratic_fee_execution( + &self, + trade_notional: i128, + kappa_execution: u64, + xi: u8, + spot_depth: u128, + ) -> DriftResult { + let scaled_abs_trade_notional = trade_notional + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(spot_depth.cast::()?)?; + + kappa_execution + .cast::()? + .safe_mul(xi.safe_mul(xi)?.cast::()?)? + .safe_mul(scaled_abs_trade_notional.safe_mul(scaled_abs_trade_notional)?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + /// returns fee in PERCENTAGE_PRECISION + pub fn get_swap_fees( + &self, + in_spot_market: &SpotMarket, + in_oracle_price: i64, + in_constituent: &Constituent, + in_amount: i128, + in_target_weight: i64, + out_spot_market: Option<&SpotMarket>, + out_oracle_price: Option, + out_constituent: Option<&Constituent>, + out_target_weight: Option, + correlation: i64, + ) -> DriftResult<(i128, i128)> { + let notional_trade_size = + in_constituent.get_notional(in_oracle_price, in_spot_market, in_amount, false)?; + let out_amount = if out_oracle_price.is_some() { + notional_trade_size + .safe_div(out_oracle_price.unwrap().cast::()?)? + .safe_mul(10_i128.pow(out_spot_market.unwrap().decimals as u32))? + } else { + 0 + }; + + // Compute scalars + let in_volatility = in_constituent.volatility; + let out_volatility = if out_constituent.is_some() { + out_constituent.unwrap().volatility + } else { + self.volatility + }; + let out_gamma_execution = if out_constituent.is_some() { + out_constituent.unwrap().gamma_execution + } else { + self.gamma_execution + }; + let out_gamma_inventory = if out_constituent.is_some() { + out_constituent.unwrap().gamma_inventory + } else { + 0 + }; + let out_xi = if out_constituent.is_some() { + out_constituent.unwrap().xi + } else { + self.xi + }; + + let in_kappa_execution = in_volatility + .safe_mul(in_volatility)? + .safe_mul(in_constituent.gamma_execution as u64)? + .safe_div(PERCENTAGE_PRECISION_U64)? + .safe_div(2u64)?; + + let out_kappa_execution = out_volatility + .safe_mul(out_volatility)? + .safe_mul(out_gamma_execution as u64)? + .safe_div(PERCENTAGE_PRECISION_U64)? + .safe_div(2u64)?; + + // Compute notional targets and errors + let in_notional_target = in_target_weight + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let in_notional_before = + in_constituent.get_notional(in_oracle_price, in_spot_market, 0, true)?; + let in_notional_after = + in_constituent.get_notional(in_oracle_price, in_spot_market, in_amount, true)?; + let in_notional_error_pre = in_notional_before.safe_sub(in_notional_target)?; + + // keep aum fixed if it's a swap for calculating post error, othwerise + // increase aum first + let in_notional_error_post = if out_spot_market.is_some() { + in_notional_after.safe_sub(in_notional_target)? + } else { + let adjusted_aum = self + .last_aum + .cast::()? + .safe_add(notional_trade_size)?; + let in_notional_target_post_mint_redeem = in_target_weight + .cast::()? + .safe_mul(adjusted_aum)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + in_notional_after.safe_sub(in_notional_target_post_mint_redeem)? + }; + + let (out_notional_target, out_notional_before, out_notional_after) = + if out_constituent.is_some() { + ( + out_target_weight + .unwrap() + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?, + out_constituent.unwrap().get_notional( + out_oracle_price.unwrap(), + out_spot_market.unwrap(), + 0, + true, + )?, + out_constituent.unwrap().get_notional( + out_oracle_price.unwrap(), + out_spot_market.unwrap(), + out_amount.safe_mul(-1)?, + true, + )?, + ) + } else { + (0_i128, 0_i128, 0_i128) + }; + + let out_notional_error_pre = out_notional_before.safe_sub(out_notional_target)?; + let out_notional_error_post = out_notional_after.safe_sub(out_notional_target)?; + + // Linear fee computation amount + let in_fee_execution_linear = self.get_linear_fee_execution( + notional_trade_size, + in_kappa_execution, + in_constituent.xi, + self.last_aum.max(MIN_AUM_EXECUTION_FEE), + )?; + + let out_fee_execution_linear = self.get_linear_fee_execution( + notional_trade_size, + out_kappa_execution, + out_xi, + self.last_aum.max(MIN_AUM_EXECUTION_FEE), + )?; + + // Quadratic fee components + let in_fee_execution_quadratic = self.get_quadratic_fee_execution( + notional_trade_size, + in_kappa_execution, + in_constituent.xi, + self.last_aum.max(MIN_AUM_EXECUTION_FEE), // use 10M at very least + )?; + let out_fee_execution_quadratic = self.get_quadratic_fee_execution( + notional_trade_size, + out_kappa_execution, + out_xi, + self.last_aum.max(MIN_AUM_EXECUTION_FEE), + )?; + let (in_quadratic_inventory_fee, out_quadratic_inventory_fee) = self + .get_quadratic_fee_inventory( + get_gamma_covar_matrix( + correlation, + in_constituent.gamma_inventory, + out_gamma_inventory, + in_constituent.volatility, + out_volatility, + )?, + [in_notional_error_pre, out_notional_error_pre], + [in_notional_error_post, out_notional_error_post], + notional_trade_size, + )?; + + msg!( + "fee breakdown - in_exec_linear: {}, in_exec_quad: {}, in_inv_quad: {}, out_exec_linear: {}, out_exec_quad: {}, out_inv_quad: {}", + in_fee_execution_linear, + in_fee_execution_quadratic, + in_quadratic_inventory_fee, + out_fee_execution_linear, + out_fee_execution_quadratic, + out_quadratic_inventory_fee + ); + let total_in_fee = in_fee_execution_linear + .safe_add(in_fee_execution_quadratic)? + .safe_add(in_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + let total_out_fee = out_fee_execution_linear + .safe_add(out_fee_execution_quadratic)? + .safe_add(out_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + + Ok(( + total_in_fee.min(MAX_SWAP_FEE.safe_div(2)?), + total_out_fee.min(MAX_SWAP_FEE.safe_div(2)?), + )) + } + + /// Returns the fee to charge for a mint or redeem in PERCENTAGE_PRECISION + pub fn get_mint_redeem_fee(&self, now: i64, is_minting: bool) -> DriftResult { + let time_since_last_rebalance = + now.safe_sub(self.last_revenue_rebalance_ts.cast::()?)?; + if is_minting { + // mint fee + self.min_mint_fee.safe_add( + self.max_mint_fee_premium.min( + self.max_mint_fee_premium + .safe_mul(time_since_last_rebalance)? + .safe_div(self.revenue_rebalance_period.cast::()?)?, + ), + ) + } else { + // burn fee + self.min_mint_fee.safe_add( + 0_i64.max( + self.max_mint_fee_premium.min( + self.revenue_rebalance_period + .cast::()? + .safe_sub(time_since_last_rebalance)? + .cast::()? + .safe_mul(self.max_mint_fee_premium.cast::()?)? + .safe_div(self.revenue_rebalance_period.cast::()?)?, + ), + ), + ) + } + } + + pub fn record_mint_redeem_fees(&mut self, amount: i64) -> DriftResult { + self.total_mint_redeem_fees_paid = self + .total_mint_redeem_fees_paid + .safe_add(amount.cast::()?)?; + Ok(()) + } + + pub fn update_aum( + &mut self, + now: i64, + slot: u64, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + constituent_target_base: &AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, + amm_cache: &AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed>, + ) -> DriftResult<(u128, i128, BTreeMap>)> { + let mut aum: u128 = 0; + let mut crypto_delta = 0_i128; + let mut oldest_slot = u64::MAX; + let mut derivative_groups: BTreeMap> = BTreeMap::new(); + for i in 0..self.constituents as usize { + let constituent = constituent_map.get_ref(&(i as u16))?; + if slot.saturating_sub(constituent.last_oracle_slot) + > constituent.oracle_staleness_threshold + { + msg!( + "Constituent {} oracle slot is too stale: {}, current slot: {}", + constituent.constituent_index, + constituent.last_oracle_slot, + slot + ); + return Err(ErrorCode::ConstituentOracleStale.into()); + } + + if constituent.constituent_derivative_index >= 0 && constituent.derivative_weight != 0 { + if !derivative_groups + .contains_key(&(constituent.constituent_derivative_index as u16)) + { + derivative_groups.insert( + constituent.constituent_derivative_index as u16, + vec![constituent.constituent_index], + ); + } else { + derivative_groups + .get_mut(&(constituent.constituent_derivative_index as u16)) + .unwrap() + .push(constituent.constituent_index); + } + } + + let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?; + + let oracle_slot = constituent.last_oracle_slot; + + if oracle_slot < oldest_slot { + oldest_slot = oracle_slot; + } + + let constituent_aum = constituent + .get_full_balance(&spot_market)? + .safe_mul(constituent.last_oracle_price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))? + .max(0); + msg!( + "constituent: {}, balance: {}, aum: {}, deriv index: {}", + constituent.constituent_index, + constituent.get_full_balance(&spot_market)?, + constituent_aum, + constituent.constituent_derivative_index + ); + + // sum up crypto deltas (notional exposures for all non-stablecoins) + if constituent.constituent_index != self.usdc_consituent_index + && constituent.constituent_derivative_index != self.usdc_consituent_index as i16 + { + let constituent_target_notional = constituent_target_base + .get(constituent.constituent_index as u32) + .target_base + .safe_mul(constituent.last_oracle_price)? + .safe_div(10_i64.pow(constituent.decimals as u32))?; + crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; + } + aum = aum.safe_add(constituent_aum.cast()?)?; + } + + let mut aum_i128 = aum.cast::()?; + for cache_datum in amm_cache.iter() { + aum_i128 -= cache_datum.quote_owed_from_lp_pool as i128; + } + aum = aum_i128.max(0i128).cast::()?; + + self.last_aum = aum; + self.last_aum_slot = slot; + self.last_aum_ts = now; + + Ok((aum, crypto_delta, derivative_groups)) + } +} + +#[zero_copy(unsafe)] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct BLPosition { + /// The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow + /// interest of corresponding market. + /// precision: token precision + pub scaled_balance: u128, + /// The cumulative deposits/borrows a user has made into a market + /// precision: token mint precision + pub cumulative_deposits: i64, + /// The market index of the corresponding spot market + pub market_index: u16, + /// Whether the position is deposit or borrow + pub balance_type: SpotBalanceType, + pub padding: [u8; 5], +} + +impl SpotBalance for BLPosition { + fn market_index(&self) -> u16 { + self.market_index + } + + fn balance_type(&self) -> &SpotBalanceType { + &self.balance_type + } + + fn balance(&self) -> u128 { + self.scaled_balance as u128 + } + + fn increase_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_add(delta)?; + Ok(()) + } + + fn decrease_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_sub(delta)?; + Ok(()) + } + + fn update_balance_type(&mut self, balance_type: SpotBalanceType) -> DriftResult { + self.balance_type = balance_type; + Ok(()) + } +} + +impl BLPosition { + pub fn get_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount(self.scaled_balance, spot_market, &self.balance_type) + } +} + +#[account(zero_copy(unsafe))] +#[derive(Default, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct Constituent { + /// address of the constituent + pub pubkey: Pubkey, + pub mint: Pubkey, + pub lp_pool: Pubkey, + pub token_vault: Pubkey, + + /// total fees received by the constituent. Positive = fees received, Negative = fees paid + pub total_swap_fees: i128, + + /// spot borrow-lend balance for constituent + pub spot_balance: BLPosition, // should be in constituent base asset + + /// max deviation from target_weight allowed for the constituent + /// precision: PERCENTAGE_PRECISION + pub max_weight_deviation: i64, + /// min fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_min: i64, + /// max fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_max: i64, + + /// Max Borrow amount: + /// precision: token precision + pub max_borrow_token_amount: u64, + + /// ata token balance in token precision + pub token_balance: u64, + + pub last_oracle_price: i64, + pub last_oracle_slot: u64, + + pub oracle_staleness_threshold: u64, + + pub flash_loan_initial_token_amount: u64, + /// Every swap to/from this constituent has a monotonically increasing id. This is the next id to use + pub next_swap_id: u64, + + /// percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight + pub derivative_weight: u64, + + pub volatility: u64, // volatility in PERCENTAGE_PRECISION 1=1% + + // depeg threshold in relation top parent in PERCENTAGE_PRECISION + pub constituent_derivative_depeg_threshold: u64, + + /// The `constituent_index` of the parent constituent. -1 if it is a parent index + /// Example: if in a pool with SOL (parent) and dSOL (derivative), + /// SOL.constituent_index = 1, SOL.constituent_derivative_index = -1, + /// dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1 + pub constituent_derivative_index: i16, + + pub spot_market_index: u16, + pub constituent_index: u16, + + pub decimals: u8, + pub bump: u8, + + // Fee params + pub gamma_inventory: u8, + pub gamma_execution: u8, + pub xi: u8, + pub _padding: [u8; 5], +} + +impl Size for Constituent { + const SIZE: usize = 304; +} + +impl Constituent { + /// Returns the full balance of the Constituent, the total of the amount in Constituent's token + /// account and in Drift Borrow-Lend. + pub fn get_full_balance(&self, spot_market: &SpotMarket) -> DriftResult { + match self.spot_balance.balance_type() { + SpotBalanceType::Deposit => self.token_balance.cast::()?.safe_add( + self.spot_balance + .get_token_amount(spot_market)? + .cast::()?, + ), + SpotBalanceType::Borrow => self.token_balance.cast::()?.safe_sub( + self.spot_balance + .get_token_amount(spot_market)? + .cast::()?, + ), + } + } + + pub fn record_swap_fees(&mut self, amount: i128) -> DriftResult { + self.total_swap_fees = self.total_swap_fees.safe_add(amount)?; + Ok(()) + } + + /// Current weight of this constituent = price * token_balance / lp_pool_aum + /// Note: lp_pool_aum is from LPPool.last_aum, which is a lagged value updated via crank + pub fn get_weight( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount_delta: i128, + lp_pool_aum: u128, + ) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let value_usd = self.get_notional(price, spot_market, token_amount_delta, true)?; + + value_usd + .safe_mul(PERCENTAGE_PRECISION_I64.cast::()?)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::() + } + + pub fn get_notional( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount: i128, + is_delta: bool, + ) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let amount = if is_delta { + let balance = self.get_full_balance(spot_market)?.cast::()?; + balance.safe_add(token_amount)? + } else { + token_amount + }; + + let value_usd = amount.safe_mul(price.cast::()?)?; + value_usd + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(PRICE_PRECISION_I128)? + .safe_div(token_precision) + } + + /// Returns the fee to charge for a swap to/from this constituent + /// The fee is a linear interpolation between the swap_fee_min and swap_fee_max based on the post-swap deviation from the target weight + /// precision: PERCENTAGE_PRECISION + pub fn get_fee_to_charge(&self, post_swap_weight: i64, target_weight: i64) -> DriftResult { + let min_weight = target_weight.safe_sub(self.max_weight_deviation as i64)?; + let max_weight = target_weight.safe_add(self.max_weight_deviation as i64)?; + let (slope_numerator, slope_denominator) = if post_swap_weight > target_weight { + let num = self.swap_fee_max.safe_sub(self.swap_fee_min)?; + let denom = max_weight.safe_sub(target_weight)?; + (num, denom) + } else { + let num = self.swap_fee_min.safe_sub(self.swap_fee_max)?; + let denom = target_weight.safe_sub(min_weight)?; + (num, denom) + }; + if slope_denominator == 0 { + return Ok(self.swap_fee_min); + } + let b = self + .swap_fee_min + .safe_mul(slope_denominator)? + .safe_sub(target_weight.safe_mul(slope_numerator)?)?; + Ok(post_swap_weight + .safe_mul(slope_numerator)? + .safe_add(b)? + .safe_div(slope_denominator)? + .clamp(self.swap_fee_min, self.swap_fee_max)) + } + + pub fn sync_token_balance(&mut self, token_account_amount: u64) { + self.token_balance = token_account_amount; + } +} + +#[zero_copy] +#[derive(Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct AmmConstituentDatum { + pub perp_market_index: u16, + pub constituent_index: u16, + pub _padding: [u8; 4], + pub last_slot: u64, + /// PERCENTAGE_PRECISION. The weight this constituent has on the perp market + pub weight: i64, +} + +impl Default for AmmConstituentDatum { + fn default() -> Self { + AmmConstituentDatum { + perp_market_index: u16::MAX, + constituent_index: u16::MAX, + _padding: [0; 4], + last_slot: 0, + weight: 0, + } + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct AmmConstituentMappingFixed { + pub lp_pool: Pubkey, + pub bump: u8, + pub _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmConstituentMappingFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmConstituentMapping { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. Each datum represents the target weight for a single (AMM, Constituent) pair. + // An AMM may be partially backed by multiple Constituents + pub weights: Vec, +} + +impl AmmConstituentMapping { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 24 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.weights.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + Ok(()) + } +} + +impl_zero_copy_loader!( + AmmConstituentMapping, + crate::id, + AmmConstituentMappingFixed, + AmmConstituentDatum +); + +#[zero_copy] +#[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, +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentTargetBaseFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentTargetBaseFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentTargetBase { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub targets: Vec, +} + +impl ConstituentTargetBase { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 24 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.targets.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + + validate!( + !self.targets.iter().any(|t| t.cost_to_trade_bps == 0), + ErrorCode::DefaultError, + "cost_to_trade_bps must be non-zero" + )?; + + Ok(()) + } +} + +impl_zero_copy_loader!( + ConstituentTargetBase, + crate::id, + ConstituentTargetBaseFixed, + TargetsDatum +); + +impl Default for ConstituentTargetBase { + fn default() -> Self { + ConstituentTargetBase { + lp_pool: Pubkey::default(), + bump: 0, + _padding: [0; 3], + targets: Vec::with_capacity(0), + } + } +} + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] +pub enum WeightValidationFlags { + NONE = 0b0000_0000, + EnforceTotalWeight100 = 0b0000_0001, + NoNegativeWeights = 0b0000_0010, + NoOverweight = 0b0000_0100, +} + +impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn get_target_weight( + &self, + constituent_index: u16, + spot_market: &SpotMarket, + price: i64, + aum: u128, + ) -> DriftResult { + validate!( + constituent_index < self.len() as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index = {}, ConstituentTargetBase len = {}", + constituent_index, + self.len() + )?; + + // TODO: validate spot market + let datum = self.get(constituent_index as u32); + let target_weight = calculate_target_weight( + datum.target_base, + &spot_market, + price, + aum, + WeightValidationFlags::NONE, + )?; + Ok(target_weight) + } +} + +pub fn calculate_target_weight( + target_base: i64, + spot_market: &SpotMarket, + price: i64, + lp_pool_aum: u128, + validation_flags: WeightValidationFlags, +) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let notional: i128 = (target_base as i128) + .safe_mul(price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + + let target_weight = notional + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::()? + .clamp(-1 * PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); + + // if (validation_flags as u8 & (WeightValidationFlags::NoNegativeWeights as u8) != 0) + // && target_weight < 0 + // { + // return Err(ErrorCode::DefaultError); + // } + // if (validation_flags as u8 & (WeightValidationFlags::NoOverweight as u8) != 0) + // && target_weight > PERCENTAGE_PRECISION_I64 as i128 + // { + // return Err(ErrorCode::DefaultError); + // } + + // if (validation_flags as u8) & WeightValidationFlags::EnforceTotalWeight100 as u8 != 0 { + // let deviation = (total_weight - PERCENTAGE_PRECISION_I128).abs(); + // let tolerance = 100; + // if deviation > tolerance { + // return Err(ErrorCode::DefaultError); + // } + // } + + Ok(target_weight) +} + +/// Update target base based on amm_inventory and mapping +impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn update_target_base( + &mut self, + mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, + // (perp market index, inventory, price) + amm_inventory_and_prices: &[(u16, i64, i64)], + constituents_indexes_and_decimals_and_prices: &[(u16, u8, i64)], + slot: u64, + ) -> DriftResult> { + let mut results = Vec::with_capacity(constituents_indexes_and_decimals_and_prices.len()); + for (i, constituent_index_and_price) in constituents_indexes_and_decimals_and_prices + .iter() + .enumerate() + { + let mut target_notional = 0i128; + let constituent_index = constituent_index_and_price.0; + let decimals = constituent_index_and_price.1; + let price = constituent_index_and_price.2; + + for (perp_market_index, inventory, price) in amm_inventory_and_prices.iter() { + let idx = mapping.iter().position(|d| { + &d.perp_market_index == perp_market_index + && d.constituent_index == constituent_index + }); + if idx.is_none() { + msg!( + "No mapping found for perp market index {} and constituent index {}", + perp_market_index, + constituent_index + ); + continue; + } + + let weight = mapping.get(idx.unwrap() as u32).weight; // PERCENTAGE_PRECISION + + let notional: i128 = (*inventory as i128) + .safe_mul(*price as i128)? + .safe_div(BASE_PRECISION_I128)?; + + target_notional += notional + .saturating_mul(weight as i128) + .saturating_div(PERCENTAGE_PRECISION_I128); + } + + let cell = self.get_mut(i as u32); + let target_base = target_notional + .safe_mul(10_i128.pow(decimals as u32))? + .safe_div(price as i128)? + * -1; // Want to target opposite sign of total scaled notional inventory + + msg!( + "updating constituent index {} target base to {} from target notional {}", + constituent_index, + target_base, + target_notional, + ); + cell.target_base = target_base.cast::()?; + cell.last_slot = slot; + + results.push(target_base); + } + + Ok(results) + } +} + +impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> { + pub fn add_amm_constituent_datum(&mut self, datum: AmmConstituentDatum) -> DriftResult<()> { + let len = self.len(); + + let mut open_slot_index: Option = None; + for i in 0..len { + let cell = self.get(i as u32); + if cell.constituent_index == datum.constituent_index + && cell.perp_market_index == datum.perp_market_index + { + return Err(ErrorCode::DefaultError); + } + if cell.last_slot == 0 && open_slot_index.is_none() { + open_slot_index = Some(i); + } + } + let open_slot = open_slot_index.ok_or_else(|| ErrorCode::DefaultError.into())?; + + let cell = self.get_mut(open_slot); + *cell = datum; + + Ok(()) + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentCorrelationsFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentCorrelationsFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentCorrelations { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub correlations: Vec, +} + +impl HasLen for ConstituentCorrelations { + fn len(&self) -> u32 { + self.correlations.len() as u32 + } +} + +impl_zero_copy_loader!( + ConstituentCorrelations, + crate::id, + ConstituentCorrelationsFixed, + i64 +); + +impl ConstituentCorrelations { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * num_constituents * 8 + } + + pub fn validate(&self) -> DriftResult<()> { + let len = self.correlations.len(); + let num_constituents = (len as f32).sqrt() as usize; // f32 is plenty precise for matrix dims < 2^16 + validate!( + num_constituents * num_constituents == self.correlations.len(), + ErrorCode::DefaultError, + "ConstituentCorrelation correlations len must be a perfect square" + )?; + + for i in 0..num_constituents { + for j in 0..num_constituents { + let corr = self.correlations[i * num_constituents + j]; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + let corr_ji = self.correlations[j * num_constituents + i]; + validate!( + corr == corr_ji, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be symmetric" + )?; + } + let corr_ii = self.correlations[i * num_constituents + i]; + validate!( + corr_ii == PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations diagonal must be PERCENTAGE_PRECISION" + )?; + } + + Ok(()) + } + + pub fn add_new_constituent(&mut self, new_constituent_correlations: &[i64]) -> DriftResult { + // Add a new constituent at index N (where N = old size), + // given a slice `new_corrs` of length `N` such that + // new_corrs[i] == correlation[i, N]. + // + // On entry: + // self.correlations.len() == N*N + // + // After: + // self.correlations.len() == (N+1)*(N+1) + let len = self.correlations.len(); + let n = (len as f64).sqrt() as usize; + validate!( + n * n == len, + ErrorCode::DefaultError, + "existing correlations len must be a perfect square" + )?; + validate!( + new_constituent_correlations.len() == n, + ErrorCode::DefaultError, + "new_corrs length must equal number of number of other constituents ({})", + n + )?; + for &c in new_constituent_correlations { + validate!( + c <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "correlation must be ≤ PERCENTAGE_PRECISION" + )?; + } + + let new_n = n + 1; + let mut buf = Vec::with_capacity(new_n * new_n); + + for i in 0..n { + buf.extend_from_slice(&self.correlations[i * n..i * n + n]); + buf.push(new_constituent_correlations[i]); + } + + buf.extend_from_slice(new_constituent_correlations); + buf.push(PERCENTAGE_PRECISION_I64); + + self.correlations = buf; + + debug_assert_eq!(self.correlations.len(), new_n * new_n); + + Ok(()) + } + + pub fn set_correlation(&mut self, i: u16, j: u16, corr: i64) -> DriftResult { + let num_constituents = (self.correlations.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + + self.correlations[(i as usize * num_constituents + j as usize) as usize] = corr; + self.correlations[(j as usize * num_constituents + i as usize) as usize] = corr; + + self.validate()?; + + Ok(()) + } +} + +impl<'a> AccountZeroCopy<'a, i64, ConstituentCorrelationsFixed> { + pub fn get_correlation(&self, i: u16, j: u16) -> DriftResult { + let num_constituents = (self.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + + let corr = self.get((i as usize * num_constituents + j as usize) as u32); + Ok(*corr) + } +} + +pub fn get_gamma_covar_matrix( + correlation_ij: i64, + gamma_i: u8, + gamma_j: u8, + vol_i: u64, + vol_j: u64, +) -> DriftResult<[[i128; 2]; 2]> { + // Build the covariance matrix + let mut covar_matrix = [[0i128; 2]; 2]; + let scaled_vol_i = vol_i as i128; + let scaled_vol_j = vol_j as i128; + covar_matrix[0][0] = scaled_vol_i + .safe_mul(scaled_vol_i)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][1] = scaled_vol_j + .safe_mul(scaled_vol_j)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[0][1] = scaled_vol_i + .safe_mul(scaled_vol_j)? + .safe_mul(correlation_ij as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][0] = covar_matrix[0][1]; + + // Build the gamma matrix as a diagonal matrix + let gamma_matrix = [[gamma_i as i128, 0i128], [0i128, gamma_j as i128]]; + + // Multiply gamma_matrix with covar_matrix: product = gamma_matrix * covar_matrix + let mut product = [[0i128; 2]; 2]; + for i in 0..2 { + for j in 0..2 { + for k in 0..2 { + product[i][j] = product[i][j] + .checked_add( + gamma_matrix[i][k] + .checked_mul(covar_matrix[k][j]) + .ok_or(ErrorCode::MathError)?, + ) + .ok_or(ErrorCode::MathError)?; + } + } + } + + Ok(product) +} + +pub fn update_constituent_target_base_for_derivatives( + aum: u128, + derivative_groups: &BTreeMap>, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, +) -> DriftResult<()> { + for (parent_index, constituent_indexes) in derivative_groups.iter() { + let parent_constituent = constituent_map.get_ref(&(parent_index))?; + let parent_target_base = constituent_target_base + .get(*parent_index as u32) + .target_base; + let target_parent_weight = calculate_target_weight( + parent_target_base, + &*spot_market_map.get_ref(&parent_constituent.spot_market_index)?, + parent_constituent.last_oracle_price, + aum, + WeightValidationFlags::NONE, + )?; + let mut derivative_weights_sum = 0; + for constituent_index in constituent_indexes { + let constituent = constituent_map.get_ref(constituent_index)?; + if constituent.last_oracle_price + < parent_constituent + .last_oracle_price + .safe_mul(constituent.constituent_derivative_depeg_threshold as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)? + { + msg!( + "Constituent {} last oracle price {} is too low compared to parent constituent {} last oracle price {}. Assuming depegging and setting target base to 0.", + constituent.constituent_index, + constituent.last_oracle_price, + parent_constituent.constituent_index, + parent_constituent.last_oracle_price + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = 0_i64; + continue; + } + + derivative_weights_sum += constituent.derivative_weight; + + let target_weight = target_parent_weight + .safe_mul(constituent.derivative_weight as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)?; + + msg!( + "constituent: {}, target weight: {}", + constituent_index, + target_weight, + ); + let target_base = aum + .cast::()? + .safe_mul(target_weight as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(constituent.decimals as u32))? + .safe_div(constituent.last_oracle_price as i128)?; + + msg!( + "constituent: {}, target base: {}", + constituent_index, + target_base + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = target_base.cast::()?; + } + + validate!( + derivative_weights_sum <= PERCENTAGE_PRECISION_U64, + ErrorCode::InvalidConstituentDerivativeWeights, + "derivative_weights_sum for parent constituent {} must be less than or equal to 100%", + parent_index + )?; + + constituent_target_base + .get_mut(*parent_index as u32) + .target_base = parent_target_base + .safe_mul(PERCENTAGE_PRECISION_U64.safe_sub(derivative_weights_sum)? as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)?; + } + + Ok(()) +} diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs new file mode 100644 index 0000000000..439fd4ccc5 --- /dev/null +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -0,0 +1,3569 @@ +#[cfg(test)] +mod tests { + use crate::math::constants::{ + BASE_PRECISION_I64, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + use std::{cell::RefCell, marker::PhantomData, vec}; + + fn amm_const_datum( + perp_market_index: u16, + constituent_index: u16, + weight: i64, + last_slot: u64, + ) -> AmmConstituentDatum { + AmmConstituentDatum { + perp_market_index, + constituent_index, + weight, + last_slot, + ..AmmConstituentDatum::default() + } + } + + #[test] + fn test_complex_implementation() { + // Constituents are BTC, SOL, ETH, USDC + + let slot = 20202020 as u64; + let amm_data = [ + amm_const_datum(0, 0, PERCENTAGE_PRECISION_I64, slot), // BTC-PERP + amm_const_datum(1, 1, PERCENTAGE_PRECISION_I64, slot), // SOL-PERP + amm_const_datum(2, 2, PERCENTAGE_PRECISION_I64, slot), // ETH-PERP + amm_const_datum(3, 0, 46 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for BTC + amm_const_datum(3, 1, 132 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for SOL + amm_const_datum(3, 2, 35 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for ETH + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 6, + ..AmmConstituentMappingFixed::default() + }); + const LEN: usize = 6; + const DATA_SIZE: usize = std::mem::size_of::() * LEN; + let defaults: [AmmConstituentDatum; LEN] = [AmmConstituentDatum::default(); LEN]; + let mapping_data = RefCell::new(unsafe { + std::mem::transmute::<[AmmConstituentDatum; LEN], [u8; DATA_SIZE]>(defaults) + }); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in amm_data { + println!("Adding AMM Constituent Datum: {:?}", amm_datum); + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_price: Vec<(u16, i64, i64)> = vec![ + (0, 4 * BASE_PRECISION_I64, 100_000 * PRICE_PRECISION_I64), // $400k BTC + (1, 2000 * BASE_PRECISION_I64, 200 * PRICE_PRECISION_I64), // $400k SOL + (2, 200 * BASE_PRECISION_I64, 1500 * PRICE_PRECISION_I64), // $300k ETH + (3, 16500 * BASE_PRECISION_I64, PRICE_PRECISION_I64), // $16.5k FARTCOIN + ]; + let constituents_indexes_and_decimals_and_prices = vec![ + (0, 6, 100_000 * PRICE_PRECISION_I64), + (1, 6, 200 * PRICE_PRECISION_I64), + (2, 6, 1500 * PRICE_PRECISION_I64), + (3, 6, PRICE_PRECISION_I64), // USDC + ]; + let aum = 2_000_000 * QUOTE_PRECISION; // $2M AUM + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 96]); + let now_ts = 1234567890; + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + let target_base = target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_price, + &constituents_indexes_and_decimals_and_prices, + now_ts, + ) + .unwrap(); + + msg!("Target Base: {:?}", target_base); + + let target_weights: Vec = target_base + .iter() + .enumerate() + .map(|(index, base)| { + calculate_target_weight( + base.cast::().unwrap(), + &SpotMarket::default_quote_market(), + amm_inventory_and_price.get(index).unwrap().2, + aum, + WeightValidationFlags::NONE, + ) + .unwrap() + }) + .collect(); + + println!("Target Weights: {:?}", target_weights); + assert_eq!(target_weights.len(), 4); + assert_eq!(target_weights[0], -203795); // 20.3% BTC + assert_eq!(target_weights[1], -210890); // 21.1% SOL + assert_eq!(target_weights[2], -152887); // 15.3% ETH + assert_eq!(target_weights[3], 0); // USDC not set if it's not in AUM update + } + + #[test] + fn test_single_zero_weight() { + let amm_datum = amm_const_datum(0, 1, 0, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000, 1_000_000)]; + let constituents_indexes_and_decimals_and_prices = vec![(1, 6, 1_000_000)]; + let aum = 1_000_000; + let now_ts = 1000; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 24]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + let totalw = target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituents_indexes_and_decimals_and_prices, + now_ts, + ) + .unwrap(); + + assert!(totalw.iter().all(|&x| x == 0)); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, 0); + assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + } + + #[test] + fn test_single_full_weight() { + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let price = PRICE_PRECISION_I64; + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, BASE_PRECISION_I64, price)]; + let constituents_indexes_and_decimals_and_prices = vec![(1, 6, price)]; + let aum = 1_000_000; + let now_ts = 1234; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 24]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + let base = target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituents_indexes_and_decimals_and_prices, + now_ts, + ) + .unwrap(); + + let weight = calculate_target_weight( + *base.get(0).unwrap() as i64, + &SpotMarket::default(), + price, + aum, + WeightValidationFlags::NONE, + ) + .unwrap(); + + assert_eq!(*base.get(0).unwrap(), -1 * 10_i128.pow(6_u32)); + assert_eq!(weight, -1000000); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + } + + #[test] + fn test_multiple_constituents_partial_weights() { + let amm_mapping_data = vec![ + amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64 / 2, 111), + amm_const_datum(0, 2, PERCENTAGE_PRECISION_I64 / 2, 111), + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: amm_mapping_data.len() as u32, + ..AmmConstituentMappingFixed::default() + }); + + // 48 = size_of::() * amm_mapping_data.len() + let mapping_data = RefCell::new([0u8; 48]); + + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in &amm_mapping_data { + mapping_zc_mut + .add_amm_constituent_datum(*amm_datum) + .unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000_000, 1_000_000)]; + let constituents_indexes_and_decimals_and_prices = + vec![(1, 6, 1_000_000), (2, 6, 1_000_000)]; + + let aum = 1_000_000; + let now_ts = 999; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: amm_mapping_data.len() as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 48]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituents_indexes_and_decimals_and_prices, + now_ts, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 2); + + for i in 0..target_zc_mut.len() { + assert_eq!( + calculate_target_weight( + target_zc_mut.get(i).target_base, + &SpotMarket::default_quote_market(), + constituents_indexes_and_decimals_and_prices + .get(i as usize) + .unwrap() + .2, + aum, + WeightValidationFlags::NONE + ) + .unwrap(), + -1 * PERCENTAGE_PRECISION_I64 / 2 + ); + assert_eq!(target_zc_mut.get(i).last_slot, now_ts); + } + } + + #[test] + fn test_zero_aum_safe() { + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000, 142_000_000)]; + let constituents_indexes_and_decimals_and_prices = vec![(1, 6, 142_000_000)]; + + let prices = vec![142_000_000]; + let aum = 0; + let now_ts = 111; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 24]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituents_indexes_and_decimals_and_prices, + now_ts, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, -1_000); // despite no aum, desire to reach target + assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + } + + #[test] + fn test_constituent_fee_to_charge() { + let mut constituent = Constituent::default(); + constituent.swap_fee_min = PERCENTAGE_PRECISION_I64 / 10000; // 1 bps + constituent.swap_fee_max = PERCENTAGE_PRECISION_I64 / 1000; // 10 bps; + constituent.max_weight_deviation = PERCENTAGE_PRECISION_I64 / 10; // max 10% deviation from target + + // target weight is 50%, push the Constituent to 40% (max below target) + let fee = constituent + .get_fee_to_charge( + PERCENTAGE_PRECISION_I64 * 40 / 100, + PERCENTAGE_PRECISION_I64 / 2, + ) + .unwrap(); + assert_eq!(fee, PERCENTAGE_PRECISION_I64 / 1000); // 10 bps + + // target weight is 50%, push the Constituent to 60% (max above target) + let fee = constituent + .get_fee_to_charge( + PERCENTAGE_PRECISION_I64 * 60 / 100, + PERCENTAGE_PRECISION_I64 / 2, + ) + .unwrap(); + assert_eq!(fee, PERCENTAGE_PRECISION_I64 / 1000); // 10 bps + + // target weight is 50%, push the Constituent to 45% (half to min target) + let fee = constituent + .get_fee_to_charge( + PERCENTAGE_PRECISION_I64 * 45 / 100, + PERCENTAGE_PRECISION_I64 / 2, + ) + .unwrap(); + assert_eq!(fee, PERCENTAGE_PRECISION_I64 * 55 / 100000); // 5.5 bps + + // target weight is 50%, push the Constituent to 55% (half to max target) + let fee = constituent + .get_fee_to_charge( + PERCENTAGE_PRECISION_I64 * 55 / 100, + PERCENTAGE_PRECISION_I64 / 2, + ) + .unwrap(); + assert_eq!(fee, PERCENTAGE_PRECISION_I64 * 55 / 100000); // 5.5 bps + + // target weight is 50%, push the Constituent to 50% (target) + let fee = constituent + .get_fee_to_charge( + PERCENTAGE_PRECISION_I64 * 50 / 100, + PERCENTAGE_PRECISION_I64 / 2, + ) + .unwrap(); + assert_eq!(fee, PERCENTAGE_PRECISION_I64 / 10000); // 1 bps (min fee) + } +} + +#[cfg(test)] +mod swap_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, SPOT_BALANCE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_swap_price() { + let lp_pool = LPPool::default(); + + let in_oracle = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let out_oracle = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + // same decimals + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &in_oracle, &out_oracle) + .unwrap(); + assert_eq!(price_num, 1_000_000); + assert_eq!(price_denom, 233_400_000); + + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &out_oracle, &in_oracle) + .unwrap(); + assert_eq!(price_num, 233_400_000); + assert_eq!(price_denom, 1_000_000); + } + + fn get_swap_amount_decimals_scenario( + in_current_weight: u64, + out_current_weight: u64, + in_decimals: u32, + out_decimals: u32, + in_amount: u64, + expected_in_amount: u128, + expected_out_amount: u128, + expected_in_fee: i128, + expected_out_fee: i128, + in_xi: u8, + out_xi: u8, + in_gamma_inventory: u8, + out_gamma_inventory: u8, + in_gamma_execution: u8, + out_gamma_execution: u8, + in_volatility: u64, + out_volatility: u64, + ) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let in_token_amount = in_notional * 10_u128.pow(in_decimals) / oracle_0.price as u128; + + let out_notional = (out_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let out_token_amount = out_notional * 10_u128.pow(out_decimals) / oracle_1.price as u128; + + let constituent_0 = Constituent { + decimals: in_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: in_gamma_execution, + gamma_inventory: in_gamma_inventory, + xi: in_xi, + volatility: in_volatility, + token_balance: in_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: out_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: out_gamma_execution, + gamma_inventory: out_gamma_inventory, + xi: out_xi, + volatility: out_volatility, + token_balance: out_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: out_decimals, + ..SpotMarket::default() + }; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + 500_000, + 500_000, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + assert_eq!(in_amount, expected_in_amount); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(in_fee, expected_in_fee); + assert_eq!(out_fee, expected_out_fee); + } + + #[test] + fn test_get_swap_amount_in_6_out_6() { + get_swap_amount_decimals_scenario( + 500_000, + 500_000, + 6, + 6, + 150_000_000_000, + 150_000_000_000, + 642577120, + 22500000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_6_out_9() { + get_swap_amount_decimals_scenario( + 500_000, + 500_000, + 6, + 9, + 150_000_000_000, + 150_000_000_000, + 642577120822, + 22500000, + 281448778, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_9_out_6() { + get_swap_amount_decimals_scenario( + 500_000, + 500_000, + 9, + 6, + 150_000_000_000_000, + 150_000_000_000_000, + 642577120, + 22500000000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_fee_to_charge_positive_min_fee() { + let c = Constituent { + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, // 1 bps + swap_fee_max: PERCENTAGE_PRECISION_I64 / 100, // 100 bps + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, // 10% + ..Constituent::default() + }; + + // swapping to target should incur minimum fee + let target_weight = PERCENTAGE_PRECISION_I64 / 2; // 50% + let post_swap_weight = target_weight; // 50% + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_min); + + // positive target: swapping to max deviation above target should incur maximum fee + let post_swap_weight = target_weight + c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // positive target: swapping to max deviation below target should incur minimum fee + let post_swap_weight = target_weight - c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // negative target: swapping to max deviation above target should incur maximum fee + let post_swap_weight = -1 * target_weight + c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // negative target: swapping to max deviation below target should incur minimum fee + let post_swap_weight = -1 * target_weight - c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // positive target: swaps to +max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = target_weight + c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + + // positive target: swaps to -max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = target_weight - c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + + // negative target: swaps to +max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = -1 * target_weight + c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + + // negative target: swaps to -max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = -1 * target_weight - c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + } + + #[test] + fn test_get_fee_to_charge_negative_min_fee() { + let c = Constituent { + swap_fee_min: -1 * PERCENTAGE_PRECISION_I64 / 10000, // -1 bps (rebate) + swap_fee_max: PERCENTAGE_PRECISION_I64 / 100, // 100 bps + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, // 10% + ..Constituent::default() + }; + + // swapping to target should incur minimum fee + let target_weight = PERCENTAGE_PRECISION_I64 / 2; // 50% + let post_swap_weight = target_weight; // 50% + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_min); + + // positive target: swapping to max deviation above target should incur maximum fee + let post_swap_weight = target_weight + c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // positive target: swapping to max deviation below target should incur minimum fee + let post_swap_weight = target_weight - c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // negative target: swapping to max deviation above target should incur maximum fee + let post_swap_weight = -1 * target_weight + c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // negative target: swapping to max deviation below target should incur minimum fee + let post_swap_weight = -1 * target_weight - c.max_weight_deviation; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, c.swap_fee_max); + + // positive target: swaps to +max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = target_weight + c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + + // positive target: swaps to -max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = target_weight - c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + + // negative target: swaps to +max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = -1 * target_weight + c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + + // negative target: swaps to -max_weight_deviation/2, should incur half of the max fee + let post_swap_weight = -1 * target_weight - c.max_weight_deviation / 2; + let fee = c + .get_fee_to_charge(post_swap_weight, -1 * target_weight) + .unwrap(); + assert_eq!(fee, (c.swap_fee_max + c.swap_fee_min) / 2); + } + + #[test] + fn test_get_weight() { + let c = Constituent { + swap_fee_min: -1 * PERCENTAGE_PRECISION_I64 / 10000, // -1 bps (rebate) + swap_fee_max: PERCENTAGE_PRECISION_I64 / 100, // 100 bps + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, // 10% + spot_market_index: 0, + spot_balance: BLPosition { + scaled_balance: 500_000, + cumulative_deposits: 1_000_000, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..BLPosition::default() + }, + token_balance: 500_000, + decimals: 6, + ..Constituent::default() + }; + + let spot_market = SpotMarket { + market_index: 0, + decimals: 6, + cumulative_deposit_interest: 10_000_000_000_000, + ..SpotMarket::default() + }; + + let full_balance = c.get_full_balance(&spot_market).unwrap(); + assert_eq!(full_balance, 1_000_000); + + // 1/10 = 10% + let weight = c + .get_weight( + 1_000_000, // $1 + &spot_market, + 0, + 10_000_000, + ) + .unwrap(); + assert_eq!(weight, 100_000); + + // (1+1)/10 = 20% + let weight = c + .get_weight(1_000_000, &spot_market, 1_000_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 200_000); + + // (1-0.5)/10 = 0.5% + let weight = c + .get_weight(1_000_000, &spot_market, -500_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 50_000); + } + + fn get_mint_redeem_fee_scenario(now: i64, is_mint: bool, expected_fee: i64) { + let lp_pool = LPPool { + last_revenue_rebalance_ts: 0, + revenue_rebalance_period: 3600, // hourly + max_mint_fee_premium: 2000, // 20 bps + min_mint_fee: 100, // 1 bps + ..LPPool::default() + }; + + let fee = lp_pool.get_mint_redeem_fee(now, is_mint).unwrap(); + assert_eq!(fee, expected_fee); + } + + #[test] + fn test_get_mint_fee_before_dist() { + get_mint_redeem_fee_scenario(0, true, 100); + } + + #[test] + fn test_get_mint_fee_during_dist() { + get_mint_redeem_fee_scenario(1800, true, 1100); + } + + #[test] + fn test_get_mint_fee_after_dist() { + get_mint_redeem_fee_scenario(3600, true, 2100); + } + + #[test] + fn test_get_redeem_fee_before_dist() { + get_mint_redeem_fee_scenario(0, false, 2100); + } + + #[test] + fn test_get_redeem_fee_during_dist() { + get_mint_redeem_fee_scenario(1800, false, 1100); + } + + #[test] + fn test_get_redeem_fee_after_dist() { + get_mint_redeem_fee_scenario(3600, false, 100); + } + + fn get_add_liquidity_mint_amount_scenario( + last_aum: u128, + now: i64, + in_decimals: u32, + in_amount: u128, + dlp_total_supply: u64, + expected_lp_amount: u64, + expected_lp_fee: i64, + expected_in_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + last_revenue_rebalance_ts: 0, + revenue_rebalance_period: 3600, + max_mint_fee_premium: 0, + min_mint_fee: 0, + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: BLPosition { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..BLPosition::default() + }, + token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount, in_amount_1, lp_fee, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + now, + &spot_market, + &constituent, + in_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount, expected_lp_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(in_amount_1, in_amount); + assert_eq!(in_fee_amount, expected_in_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_add_liquidity_mint_amount_zero_aum() { + get_add_liquidity_mint_amount_scenario( + 0, // last_aum + 0, // now + 6, // in_decimals + 1_000_000, // in_amount + 0, // dlp_total_supply (non-zero to avoid MathError) + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum() { + get_add_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 300, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, // last_aum + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount in lp decimals + 0, // expected_lp_fee + 30000, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 4 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, // last_aum + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1000000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 3, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + fn get_remove_liquidity_mint_amount_scenario( + last_aum: u128, + now: i64, + in_decimals: u32, + lp_burn_amount: u64, + dlp_total_supply: u64, + expected_out_amount: u128, + expected_lp_fee: i64, + expected_out_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + last_revenue_rebalance_ts: 0, + revenue_rebalance_period: 3600, + max_mint_fee_premium: 2000, // 20 bps + min_mint_fee: 100, // 1 bps + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: BLPosition { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..BLPosition::default() + }, + token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount_1, out_amount, lp_fee, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + now, + &spot_market, + &constituent, + lp_burn_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount_1, lp_burn_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(out_fee_amount, expected_out_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 997900, // expected_out_amount + 2100, // expected_lp_fee + 299, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 9979000000, // expected_out_amount + 210000, // expected_lp_fee + 2993700, + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 4 decimal constituent + // there will be a problem with 4 decimal constituents with aum ~10M + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = 1/10000 + 10_000_000_000, // dlp_total_supply + 99, // expected_out_amount + 21, // expected_lp_fee + 0, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_5_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 5, // in_decimals + 100_000_000_000 * 100_000, // in_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 997900000000000, // expected_out_amount + 21000000000000, // expected_lp_fee + 473004600000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_6_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 6, // in_decimals + 100_000_000_000 * 1_000_000, // in_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 99790000000000000, // expected_out_amount + 210000000000000, // expected_lp_fee + 348167310000000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 10_000_000_000_000_000, // last_aum ($10,000,000,000) + 0, // now + 8, // in_decimals + 10_000_000_000 * 100_000_000, // in_amount + 10_000_000_000 * 1_000_000, // dlp_total_supply + 9_979_000_000_000_000_0000, // expected_out_amount + 2100_000_000_000_000, // expected_lp_fee + 3757093500000000000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + fn round_to_sig(x: i128, sig: u32) -> i128 { + if x == 0 { + return 0; + } + let digits = (x.abs() as f64).log10().floor() as u32 + 1; + let factor = 10_i128.pow(digits - sig); + ((x + factor / 2) / factor) * factor + } + + fn get_swap_amounts( + in_oracle_price: i64, + out_oracle_price: i64, + in_current_weight: i64, + out_current_weight: i64, + in_amount: u64, + in_volatility: u64, + out_volatility: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> (u128, u128, i128, i128, i128, i128) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: in_oracle_price, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: out_oracle_price, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let in_token_amount = in_notional * 10_i128.pow(6) / oracle_0.price as i128; + let in_spot_balance = if in_token_amount > 0 { + BLPosition { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..BLPosition::default() + } + } else { + BLPosition { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Borrow, + market_index: 0, + ..BLPosition::default() + } + }; + + let out_notional = (out_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let out_token_amount = out_notional * 10_i128.pow(6) / oracle_1.price as i128; + let out_spot_balance = if out_token_amount > 0 { + BLPosition { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..BLPosition::default() + } + } else { + BLPosition { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..BLPosition::default() + } + }; + + let constituent_0 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 1, + gamma_inventory: 1, + xi: 1, + volatility: in_volatility, + spot_balance: in_spot_balance, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 2, + gamma_inventory: 2, + xi: 2, + volatility: out_volatility, + spot_balance: out_spot_balance, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + + let (in_amount_result, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + in_target_weight, + out_target_weight, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + + return ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount, + out_token_amount, + ); + } + + #[test] + fn grid_search_swap() { + let weights: [i64; 20] = [ + -100_000, -200_000, -300_000, -400_000, -500_000, -600_000, -700_000, -800_000, + -900_000, -1_000_000, 100_000, 200_000, 300_000, 400_000, 500_000, 600_000, 700_000, + 800_000, 900_000, 1_000_000, + ]; + let in_amounts: Vec = (0..=10) + .map(|i| (1000 + i * 20000) * 10_u64.pow(6)) + .collect(); + + let volatilities: Vec = (1..=10) + .map(|i| PERCENTAGE_PRECISION_U64 * i / 100) + .collect(); + + let in_oracle_price = PRICE_PRECISION_I64; // $1 + let out_oracle_price = 233_400_000; // $233.4 + + // Assert monotonically increasing fees in in_amounts + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + for out_volatility in volatilities.iter() { + let mut prev_in_fee_bps = 0_i128; + let mut prev_out_fee_bps = 0_i128; + for in_amount in in_amounts.iter() { + let ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount_pre, + out_token_amount_pre, + ) = get_swap_amounts( + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + 0, + *out_volatility, + PERCENTAGE_PRECISION_I64, // 100% target weight + PERCENTAGE_PRECISION_I64, // 100% target weight + ); + + // Calculate fee in basis points with precision + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + // Assert monotonically increasing fees + if in_amounts.iter().position(|&x| x == *in_amount).unwrap() > 0 { + assert!( + in_fee_bps >= prev_in_fee_bps, + "in_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_amount, + out_volatility + ); + assert!( + out_fee_bps >= prev_out_fee_bps, + "out_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + in_amount, + out_volatility + ); + } + + println!( + "in_weight: {}, out_weight: {}, in_amount: {}, out_amount: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_amount_result, + out_amount, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + + prev_in_fee_bps = in_fee_bps; + prev_out_fee_bps = out_fee_bps; + } + } + } + + // Assert monotonically increasing fees based on error improvement + for in_amount in in_amounts.iter() { + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + let fixed_volatility = PERCENTAGE_PRECISION_U64 * 5 / 100; + let target_weights: Vec = (1..=20).map(|i| i * 50_000).collect(); + + let mut results: Vec<(i128, i128, i128, i128, i128, i128)> = Vec::new(); + + for target_weight in target_weights.iter() { + let in_target_weight = *target_weight; + let out_target_weight = 1_000_000 - in_target_weight; + + let ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount_pre, + out_token_amount_pre, + ) = get_swap_amounts( + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + fixed_volatility, + fixed_volatility, + in_target_weight, + out_target_weight, + ); + + // Calculate weights after swap + + let out_token_after = out_token_amount_pre - out_amount as i128 + out_fee; + let in_token_after = in_token_amount_pre + in_amount_result as i128; + + let out_notional_after = + out_token_after * (out_oracle_price as i128) / PRICE_PRECISION_I128; + let in_notional_after = + in_token_after * (in_oracle_price as i128) / PRICE_PRECISION_I128; + let total_notional_after = in_notional_after + out_notional_after; + + let out_weight_after = + (out_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + let in_weight_after = + (in_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + + // Calculate error improvement (positive means improvement) + let in_error_before = (*in_current_weight - in_target_weight).abs() as i128; + let out_error_before = (out_current_weight - out_target_weight).abs() as i128; + + let in_error_after = (in_weight_after - in_target_weight as i128).abs(); + let out_error_after = (out_weight_after - out_target_weight as i128).abs(); + + let in_error_improvement = round_to_sig(in_error_before - in_error_after, 2); + let out_error_improvement = round_to_sig(out_error_before - out_error_after, 2); + + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + results.push(( + in_error_improvement, + out_error_improvement, + in_fee_bps, + out_fee_bps, + in_target_weight as i128, + out_target_weight as i128, + )); + + println!( + "in_weight: {}, out_weight: {}, in_target: {}, out_target: {}, in_error_improvement: {}, out_error_improvement: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_target_weight, + out_target_weight, + in_error_improvement, + out_error_improvement, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + } + + // Sort by in_error_improvement and check monotonicity + results.sort_by_key(|&(in_error_improvement, _, _, _, _, _)| -in_error_improvement); + + for i in 1..results.len() { + let (prev_in_improvement, _, prev_in_fee_bps, _, _, _) = results[i - 1]; + let (curr_in_improvement, _, curr_in_fee_bps, _, in_target, _) = results[i]; + + // Less improvement should mean higher fees + if curr_in_improvement < prev_in_improvement { + assert!( + curr_in_fee_bps >= prev_in_fee_bps, + "in_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, in_weight: {}, in_target: {}", + curr_in_improvement, + prev_in_improvement, + curr_in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_target + ); + } + } + + // Sort by out_error_improvement and check monotonicity + results + .sort_by_key(|&(_, out_error_improvement, _, _, _, _)| -out_error_improvement); + + for i in 1..results.len() { + let (_, prev_out_improvement, _, prev_out_fee_bps, _, _) = results[i - 1]; + let (_, curr_out_improvement, _, curr_out_fee_bps, _, out_target) = results[i]; + + // Less improvement should mean higher fees + if curr_out_improvement < prev_out_improvement { + assert!( + curr_out_fee_bps >= prev_out_fee_bps, + "out_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, out_weight: {}, out_target: {}", + curr_out_improvement, + prev_out_improvement, + curr_out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + out_target + ); + } + } + } + } + } +} + +#[cfg(test)] +mod swap_fee_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_gamma_covar_matrix() { + // in = sol, out = btc + let covar_matrix = get_gamma_covar_matrix( + PERCENTAGE_PRECISION_I64, + 2, // gamma sol + 2, // gamma btc + 4 * PERCENTAGE_PRECISION_U64 / 100, // vol sol + 3 * PERCENTAGE_PRECISION_U64 / 100, // vol btc + ) + .unwrap(); + assert_eq!(covar_matrix, [[3200, 2400], [2400, 1800]]); + } + + #[test] + fn test_lp_pool_get_linear_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let fee_execution_linear = lp_pool + .get_linear_fee_execution( + 5_000_000 * QUOTE_PRECISION_I128, + 1600, // 0.0016 + 2, + 15_000_000 * QUOTE_PRECISION, + ) + .unwrap(); + + assert_eq!(fee_execution_linear, 1066); // 10.667 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let fee_execution_quadratic = lp_pool + .get_quadratic_fee_execution( + 5_000_000 * QUOTE_PRECISION_I128, + 1600, // 0.0016 + 2, + 15_000_000 * QUOTE_PRECISION, + ) + .unwrap(); + + assert_eq!(fee_execution_quadratic, 711); // 7.1 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_inventory() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let (fee_in, fee_out) = lp_pool + .get_quadratic_fee_inventory( + [[3200, 2400], [2400, 1800]], + [ + 1_000_000 * QUOTE_PRECISION_I128, + -500_000 * QUOTE_PRECISION_I128, + ], + [ + -4_000_000 * QUOTE_PRECISION_I128, + 4_500_000 * QUOTE_PRECISION_I128, + ], + 5_000_000 * QUOTE_PRECISION_I128, + ) + .unwrap(); + + assert_eq!(fee_in, 6 * PERCENTAGE_PRECISION_I128 / 100000); // 0.6 bps + assert_eq!(fee_out, -6 * PERCENTAGE_PRECISION_I128 / 100000); // -0.6 bps + } +} + +#[cfg(test)] +mod settle_tests { + use crate::math::lp_pool::perp_lp_pool_settlement::{ + calculate_settlement_amount, update_cache_info, SettlementContext, SettlementDirection, + SettlementResult, + }; + use crate::state::perp_market::CacheInfo; + use crate::state::spot_market::SpotMarket; + + fn create_mock_spot_market() -> SpotMarket { + SpotMarket::default() + } + + #[test] + fn test_calculate_settlement_no_amount_owed() { + let ctx = SettlementContext { + quote_owed_from_lp: 0, + quote_constituent_token_balance: 1000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_lp_to_perp_settlement_sufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_lp_to_perp_settlement_insufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 1500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000); // Limited by LP balance + } + + #[test] + fn test_lp_to_perp_settlement_no_lp_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500, + quote_constituent_token_balance: 0, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_perp_to_lp_settlement_fee_pool_sufficient() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 800, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_settlement_needs_both_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 700); + } + + #[test] + fn test_perp_to_lp_settlement_insufficient_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1500, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); // Limited by pool balances + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 200); + } + + #[test] + fn test_settlement_edge_cases() { + // Test with zero fee pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 0, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 500); + + // Test with zero pnl pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 300); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_update_cache_info_to_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: -400, + last_fee_pool_token_amount: 2_000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 0, + last_settle_ts: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 200, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 120, + pnl_pool_used: 80, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + result.amount_transferred as i64; + let ts = 99; + + update_cache_info(&mut cache, &result, new_quote_owed, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 200); + assert_eq!(cache.last_settle_ts, ts); + // fee pool decreases by fee_pool_used + assert_eq!(cache.last_fee_pool_token_amount, 2_000 - 120); + // pnl pool decreases by pnl_pool_used + assert_eq!(cache.last_net_pnl_pool_token_amount, 500 - 80); + } + + #[test] + fn test_update_cache_info_from_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 500, + last_fee_pool_token_amount: 1_000, + last_net_pnl_pool_token_amount: 200, + last_settle_amount: 0, + last_settle_ts: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 150, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - result.amount_transferred as i64; + let ts = 42; + + update_cache_info(&mut cache, &result, new_quote_owed, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 150); + assert_eq!(cache.last_settle_ts, ts); + // fee pool increases by amount_transferred + assert_eq!(cache.last_fee_pool_token_amount, 1_000 + 150); + // pnl pool untouched + assert_eq!(cache.last_net_pnl_pool_token_amount, 200); + } + + #[test] + fn test_large_settlement_amounts() { + // Test with very large amounts to check for overflow + let ctx = SettlementContext { + quote_owed_from_lp: i64::MAX / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_negative_large_settlement_amounts() { + let ctx = SettlementContext { + quote_owed_from_lp: i64::MIN / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_exact_boundary_settlements() { + // Test when quote_owed exactly equals LP balance + let ctx = SettlementContext { + quote_owed_from_lp: 1000, + quote_constituent_token_balance: 1000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000); + + // Test when negative quote_owed exactly equals total pool balance + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 300); + } + + #[test] + fn test_minimal_settlement_amounts() { + // Test with minimal positive amount + let ctx = SettlementContext { + quote_owed_from_lp: 1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 1, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1); + + // Test with minimal negative amount + let ctx = SettlementContext { + quote_owed_from_lp: -1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1); + assert_eq!(result.fee_pool_used, 1); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_all_zero_balances() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 0, + fee_pool_balance: 0, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 0); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_cache_info_update_none_direction() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 100, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 50, + last_settle_ts: 12345, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = 100; // No change + let ts = 67890; + + update_cache_info(&mut cache, &result, new_quote_owed, ts).unwrap(); + + // quote_owed unchanged + assert_eq!(cache.quote_owed_from_lp_pool, 100); + // settle fields updated with new timestamp but zero amount + assert_eq!(cache.last_settle_amount, 0); + assert_eq!(cache.last_settle_ts, ts); + // pool amounts unchanged + assert_eq!(cache.last_fee_pool_token_amount, 1000); + assert_eq!(cache.last_net_pnl_pool_token_amount, 500); + } + + #[test] + fn test_cache_info_update_maximum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MAX / 2, + last_fee_pool_token_amount: u128::MAX / 2, + last_net_pnl_pool_token_amount: i128::MAX / 2, + last_settle_amount: 0, + last_settle_ts: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: u64::MAX / 4, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - (result.amount_transferred as i64); + let ts = i64::MAX / 2; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_cache_info_update_minimum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MIN / 2, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: i128::MIN / 2, + last_settle_amount: 0, + last_settle_ts: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 500, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 200, + pnl_pool_used: 300, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + (result.amount_transferred as i64); + let ts = 42; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_sequential_settlement_updates() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 1000, + last_fee_pool_token_amount: 5000, + last_net_pnl_pool_token_amount: 3000, + last_settle_amount: 0, + last_settle_ts: 0, + ..Default::default() + }; + + // First settlement: From LP pool + let result1 = SettlementResult { + amount_transferred: 300, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed1 = cache.quote_owed_from_lp_pool - (result1.amount_transferred as i64); + update_cache_info(&mut cache, &result1, new_quote_owed1, 100).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 700); + assert_eq!(cache.last_fee_pool_token_amount, 5300); + assert_eq!(cache.last_net_pnl_pool_token_amount, 3000); + + // Second settlement: To LP pool + let result2 = SettlementResult { + amount_transferred: 400, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 250, + pnl_pool_used: 150, + }; + let new_quote_owed2 = cache.quote_owed_from_lp_pool + (result2.amount_transferred as i64); + update_cache_info(&mut cache, &result2, new_quote_owed2, 200).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 1100); + assert_eq!(cache.last_fee_pool_token_amount, 5050); + assert_eq!(cache.last_net_pnl_pool_token_amount, 2850); + assert_eq!(cache.last_settle_ts, 200); + } + + #[test] + fn test_perp_to_lp_with_only_pnl_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 1000); + } + + #[test] + fn test_perp_to_lp_with_only_fee_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 1500, + fee_pool_balance: 1000, + pnl_pool_balance: 0, // No PnL pool + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 800); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_fractional_settlement_coverage() { + // Test when pools can only partially cover the needed amount + let ctx = SettlementContext { + quote_owed_from_lp: -2000, + quote_constituent_token_balance: 5000, + fee_pool_balance: 300, + pnl_pool_balance: 500, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); // Only what pools can provide + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_settlement_direction_consistency() { + // Positive quote_owed should always result in FromLpPool or None + for quote_owed in [1, 100, 1000, 10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::FromLpPool + || result.direction == SettlementDirection::None + ); + } + + // Negative quote_owed should always result in ToLpPool or None + for quote_owed in [-1, -100, -1000, -10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::ToLpPool + || result.direction == SettlementDirection::None + ); + } + } + + #[test] + fn test_cache_info_timestamp_progression() { + let mut cache = CacheInfo::default(); + + let timestamps = [1000, 2000, 3000, 1500, 5000]; // Including out-of-order + + for (_, &ts) in timestamps.iter().enumerate() { + let result = SettlementResult { + amount_transferred: 100, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + + update_cache_info(&mut cache, &result, 0, ts).unwrap(); + assert_eq!(cache.last_settle_ts, ts); + assert_eq!(cache.last_settle_amount, 100); + } + } + + #[test] + fn test_settlement_amount_conservation() { + // Test that fee_pool_used + pnl_pool_used = amount_transferred for ToLpPool + let test_cases = [ + (-500, 1000, 300, 400), // Normal case + (-1000, 2000, 600, 500), // Uses both pools + (-200, 500, 0, 300), // Only PnL pool + (-150, 400, 200, 0), // Only fee pool + ]; + + for (quote_owed, lp_balance, fee_pool, pnl_pool) in test_cases { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: lp_balance, + fee_pool_balance: fee_pool, + pnl_pool_balance: pnl_pool, + quote_market: &create_mock_spot_market(), + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + + if result.direction == SettlementDirection::ToLpPool { + assert_eq!( + result.amount_transferred as u128, + result.fee_pool_used + result.pnl_pool_used, + "Amount transferred should equal sum of pool usage for case: {:?}", + (quote_owed, lp_balance, fee_pool, pnl_pool) + ); + } + } + } + + #[test] + fn test_cache_pool_balance_tracking() { + let mut cache = CacheInfo { + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + ..Default::default() + }; + + // Multiple settlements that should maintain balance consistency + let settlements = [ + (SettlementDirection::ToLpPool, 200, 120, 80), // Uses both pools + (SettlementDirection::FromLpPool, 150, 0, 0), // Adds to fee pool + (SettlementDirection::ToLpPool, 100, 100, 0), // Uses only fee pool + (SettlementDirection::ToLpPool, 50, 30, 20), // Uses both pools again + ]; + + let mut expected_fee_pool = cache.last_fee_pool_token_amount; + let mut expected_pnl_pool = cache.last_net_pnl_pool_token_amount; + + for (direction, amount, fee_used, pnl_used) in settlements { + let result = SettlementResult { + amount_transferred: amount, + direction, + fee_pool_used: fee_used, + pnl_pool_used: pnl_used, + }; + + match direction { + SettlementDirection::FromLpPool => { + expected_fee_pool += amount as u128; + } + SettlementDirection::ToLpPool => { + expected_fee_pool -= fee_used; + expected_pnl_pool -= pnl_used as i128; + } + SettlementDirection::None => {} + } + + update_cache_info(&mut cache, &result, 0, 1000).unwrap(); + + assert_eq!(cache.last_fee_pool_token_amount, expected_fee_pool); + assert_eq!(cache.last_net_pnl_pool_token_amount, expected_pnl_pool); + } + } +} + +#[cfg(test)] +mod update_aum_tests { + use crate::{ + create_anchor_account_info, + math::constants::SPOT_CUMULATIVE_INTEREST_PRECISION, + math::constants::{PRICE_PRECISION_I64, QUOTE_PRECISION}, + state::lp_pool::*, + state::oracle::HistoricalOracleData, + state::oracle::OracleSource, + state::perp_market::{AmmCacheFixed, CacheInfo}, + state::spot_market::SpotMarket, + state::spot_market_map::SpotMarketMap, + state::zero_copy::AccountZeroCopyMut, + test_utils::{create_account_info, get_anchor_account_bytes}, + }; + use anchor_lang::prelude::Pubkey; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_aum_with_balances( + usdc_balance: u64, // USDC balance in tokens (6 decimals) + sol_balance: u64, // SOL balance in tokens (9 decimals) + btc_balance: u64, // BTC balance in tokens (8 decimals) + bonk_balance: u64, // BONK balance in tokens (5 decimals) + expected_aum_usd: u64, + test_name: &str, + ) { + let mut lp_pool = LPPool::default(); + lp_pool.constituents = 4; + lp_pool.usdc_consituent_index = 0; + + // Create constituents with specified token balances + let mut constituent_usdc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 0, + constituent_index: 0, + last_oracle_price: PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 6, + token_balance: usdc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_usdc, Constituent, constituent_usdc_account_info); + + let mut constituent_sol = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 1, + constituent_index: 1, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + token_balance: sol_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_sol, Constituent, constituent_sol_account_info); + + let mut constituent_btc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 2, + constituent_index: 2, + last_oracle_price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 8, + token_balance: btc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_btc, Constituent, constituent_btc_account_info); + + let mut constituent_bonk = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 3, + constituent_index: 3, + last_oracle_price: 22, // $0.000022 in PRICE_PRECISION_I64 + last_oracle_slot: 100, + decimals: 5, + token_balance: bonk_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_bonk, Constituent, constituent_bonk_account_info); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &constituent_usdc_account_info, + &constituent_sol_account_info, + &constituent_btc_account_info, + &constituent_bonk_account_info, + ], + true, + ) + .unwrap(); + + // Create spot markets + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + + let mut btc_spot_market = SpotMarket { + market_index: 2, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 8, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!(btc_spot_market, SpotMarket, btc_spot_market_account_info); + + let mut bonk_spot_market = SpotMarket { + market_index: 3, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 5, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!(bonk_spot_market, SpotMarket, bonk_spot_market_account_info); + + let spot_market_account_infos = vec![ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + &btc_spot_market_account_info, + &bonk_spot_market_account_info, + ]; + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 96]); // 4 * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Create AMM cache + let mut cache_fixed_default = AmmCacheFixed::default(); + cache_fixed_default.len = 0; // No perp markets for this test + let cache_fixed = RefCell::new(cache_fixed_default); + let cache_data = RefCell::new([0u8; 0]); // Empty cache data + let amm_cache = AccountZeroCopyMut::<'_, CacheInfo, AmmCacheFixed> { + fixed: cache_fixed.borrow_mut(), + data: cache_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Call update_aum + let result = lp_pool.update_aum( + 1000, // now (timestamp) + 101, // slot + &constituent_map, + &spot_market_map, + &constituent_target_base, + &amm_cache, + ); + + assert!(result.is_ok(), "{}: update_aum should succeed", test_name); + let (aum, crypto_delta, derivative_groups) = result.unwrap(); + + // Convert expected USD to quote precision + let expected_aum = expected_aum_usd as u128 * QUOTE_PRECISION; + + println!( + "{}: AUM = ${}, Expected = ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION + ); + + // Verify the results (allow small rounding differences) + let aum_diff = if aum > expected_aum { + aum - expected_aum + } else { + expected_aum - aum + }; + assert!( + aum_diff <= QUOTE_PRECISION, // Allow up to $1 difference for rounding + "{}: AUM mismatch. Got: ${}, Expected: ${}, Diff: ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION, + aum_diff / QUOTE_PRECISION + ); + + assert_eq!(crypto_delta, 0, "{}: crypto_delta should be 0", test_name); + assert!( + derivative_groups.is_empty(), + "{}: derivative_groups should be empty", + test_name + ); + + // Verify LP pool state was updated + assert_eq!( + lp_pool.last_aum, aum, + "{}: last_aum should match calculated AUM", + test_name + ); + assert_eq!( + lp_pool.last_aum_slot, 101, + "{}: last_aum_slot should be updated", + test_name + ); + assert_eq!( + lp_pool.last_aum_ts, 1000, + "{}: last_aum_ts should be updated", + test_name + ); + assert_eq!( + lp_pool.oldest_oracle_slot, 100, + "{}: oldest_oracle_slot should be updated", + test_name + ); + } + + #[test] + fn test_aum_zero() { + test_aum_with_balances( + 0, // 0 USDC + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 0, // $0 expected AUM + "Zero AUM", + ); + } + + #[test] + fn test_aum_low_1k() { + test_aum_with_balances( + 1_000_000_000, // 1,000 USDC (6 decimals) = $1,000 + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 1_000, // $1,000 expected AUM + "Low AUM (~$1k)", + ); + } + + #[test] + fn test_aum_reasonable() { + test_aum_with_balances( + 1_000_000_000_000, // 1M USDC (6 decimals) = $1M + 5_000_000_000_000, // 5k SOL (9 decimals) = $1M at $200/SOL + 800_000_000, // 8 BTC (8 decimals) = $800k at $100k/BTC + 0, // 0 BONK + 2_800_000, // Expected AUM based on actual calculation + "Reasonable AUM (~$2.8M)", + ); + } + + #[test] + fn test_aum_high() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 0, // 0 BONK + 210_000_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b)", + ); + } + + #[test] + fn test_aum_with_small_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000, // 1B BONK (5 decimals) = $22k at $0.000022/BONK + 210_000_022_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } + + #[test] + fn test_aum_with_large_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000_000, // 1T BONK (5 decimals) = $22M at $0.000022/BONK + 210_022_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } +} + +#[cfg(test)] +mod update_constituent_target_base_for_derivatives_tests { + use super::super::update_constituent_target_base_for_derivatives; + use crate::create_anchor_account_info; + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, QUOTE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, + }; + use crate::state::constituent_map::ConstituentMap; + use crate::state::lp_pool::{Constituent, ConstituentTargetBaseFixed, TargetsDatum}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::spot_market::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::zero_copy::AccountZeroCopyMut; + use crate::test_utils::{create_account_info, get_anchor_account_bytes}; + use anchor_lang::prelude::Pubkey; + use anchor_lang::Owner; + use std::collections::BTreeMap; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_derivative_weights_scenario( + derivative_weights: Vec, + test_name: &str, + should_succeed: bool, + ) { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, // Parent doesn't have derivative weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create first derivative constituent + let derivative1_index = parent_index + 1; // 2 + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, // $195 (slightly below parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(0).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Create second derivative constituent + let derivative2_index = parent_index + 2; // 3 + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 205 * PRICE_PRECISION_I64, // $205 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(1).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + // Create third derivative constituent + let derivative3_index = parent_index + 3; // 4 + let mut derivative3_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative3_index, + constituent_index: derivative3_index, + last_oracle_price: 210 * PRICE_PRECISION_I64, // $210 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(2).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative3_constituent, + Constituent, + derivative3_constituent_account_info + ); + + let constituents_list = vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + &derivative3_constituent_account_info, + ]; + let constituent_map = ConstituentMap::load_multiple(constituents_list, true).unwrap(); + + // Create spot markets + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let mut derivative3_spot_market = SpotMarket { + market_index: derivative3_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default(), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative3_spot_market, + SpotMarket, + derivative3_spot_market_account_info + ); + + let spot_market_list = vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + &derivative3_spot_market_account_info, + ]; + let spot_market_map = SpotMarketMap::load_multiple(spot_market_list, true).unwrap(); + + // Create constituent target base + let num_constituents = 4; // Fixed: parent + 3 derivatives + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: num_constituents as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 120]); // 4+1 constituents * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial parent target base (targeting 10% of total AUM worth of SOL tokens) + // For 10M AUM and $200 SOL price with 9 decimals: (10M * 0.1) / 200 * 10^9 = 5,000,000,000,000 tokens + let initial_parent_target_base = 5_000_000_000_000i64; // ~$1M worth of SOL tokens + constituent_target_base + .get_mut(parent_index as u32) + .target_base = initial_parent_target_base; + constituent_target_base + .get_mut(parent_index as u32) + .last_slot = 100; + + // Initialize derivative target bases to 0 + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative1_index as u32) + .last_slot = 100; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative2_index as u32) + .last_slot = 100; + constituent_target_base + .get_mut(derivative3_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative3_index as u32) + .last_slot = 100; + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + let mut active_derivatives = Vec::new(); + for (i, _) in derivative_weights.iter().enumerate() { + // Add all derivatives regardless of weight (they may have zero weight for testing) + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + active_derivatives.push(derivative_index); + } + if !active_derivatives.is_empty() { + derivative_groups.insert(parent_index, active_derivatives); + } + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok() == should_succeed, + "{}: update_constituent_target_base_for_derivatives should succeed", + test_name + ); + + if !should_succeed { + return; + } + + // Verify results + let parent_target_base_after = constituent_target_base.get(parent_index as u32).target_base; + let total_derivative_weight: u64 = derivative_weights.iter().sum(); + let remaining_parent_weight = PERCENTAGE_PRECISION_U64 - total_derivative_weight; + + // Expected parent target base after scaling down + let expected_parent_target_base = initial_parent_target_base + * (remaining_parent_weight as i64) + / (PERCENTAGE_PRECISION_I64); + + println!( + "{}: Original parent target base: {}, After: {}, Expected: {}", + test_name, + initial_parent_target_base, + parent_target_base_after, + expected_parent_target_base + ); + + assert_eq!( + parent_target_base_after, expected_parent_target_base, + "{}: Parent target base should be scaled down correctly", + test_name + ); + + // Verify derivative target bases + for (i, derivative_weight) in derivative_weights.iter().enumerate() { + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + + if *derivative_weight == 0 { + // If derivative weight is 0, target base should remain 0 + assert_eq!( + derivative_target_base, 0, + "{}: Derivative {} with zero weight should have target base 0", + test_name, derivative_index + ); + continue; + } + + // For simplicity, just verify that the derivative target base is positive and reasonable + // The exact calculation is complex and depends on the internal implementation + println!( + "{}: Derivative {} target base: {}, Weight: {}", + test_name, derivative_index, derivative_target_base, derivative_weight + ); + + assert!( + derivative_target_base > 0, + "{}: Derivative {} target base should be positive", + test_name, + derivative_index + ); + + // Verify that target base is reasonable (not too large or too small) + assert!( + derivative_target_base < 10_000_000_000_000i64, + "{}: Derivative {} target base should be reasonable", + test_name, + derivative_index + ); + } + } + + fn test_depeg_scenario() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create derivative constituent that's depegged - must have different index than parent + let derivative_index = parent_index + 1; // 2 + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Create spot markets + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 72]); // 2+1 constituents * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial values + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 2_500_000_000_000i64; // ~$500k worth of SOL + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 1_250_000_000_000i64; // ~$250k worth + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "depeg scenario: update_constituent_target_base_for_derivatives should succeed" + ); + + // Verify that depegged derivative has target base set to 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "depeg scenario: Depegged derivative should have target base 0" + ); + + // Verify that parent target base is unchanged since derivative weight is 0 now + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + assert_eq!( + parent_target_base, 2_500_000_000_000i64, + "depeg scenario: Parent target base should remain unchanged" + ); + } + + #[test] + fn test_derivative_depeg_scenario() { + // Test case: Test depeg scenario + test_depeg_scenario(); + } + + #[test] + fn test_derivative_weights_sum_to_110_percent() { + // Test case: Derivative constituents with weights that sum to 1.1 (110%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 300_000, // 30% weight + ], + "weights sum to 110%", + false, + ); + } + + #[test] + fn test_derivative_weights_sum_to_100_percent() { + // Test case: Derivative constituents with weights that sum to 1 (100%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 200_000, // 20% weight + ], + "weights sum to 100%", + true, + ); + } + + #[test] + fn test_derivative_weights_sum_to_75_percent() { + // Test case: Derivative constituents with weights that sum to < 1 (75%) + test_derivative_weights_scenario( + vec![ + 400_000, // 40% weight + 200_000, // 20% weight + 150_000, // 15% weight + ], + "weights sum to 75%", + true, + ); + } + + #[test] + fn test_single_derivative_60_percent_weight() { + // Test case: Single derivative with partial weight + test_derivative_weights_scenario( + vec![ + 600_000, // 60% weight + ], + "single derivative 60% weight", + true, + ); + } + + #[test] + fn test_single_derivative_100_percent_weight() { + // Test case: Single derivative with 100% weight - parent should become 0 + test_derivative_weights_scenario( + vec![ + 1_000_000, // 100% weight + ], + "single derivative 100% weight", + true, + ); + } + + #[test] + fn test_mixed_zero_and_nonzero_weights() { + // Test case: Mix of zero and non-zero weights + test_derivative_weights_scenario( + vec![ + 0, // 0% weight + 400_000, // 40% weight + 0, // 0% weight + ], + "mixed zero and non-zero weights", + true, + ); + } + + #[test] + fn test_very_small_weights() { + // Test case: Very small weights (1 basis point = 0.01%) + test_derivative_weights_scenario( + vec![ + 100, // 0.01% weight + 200, // 0.02% weight + 300, // 0.03% weight + ], + "very small weights", + true, + ); + } + + #[test] + fn test_zero_parent_target_base() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + let derivative_index = parent_index + 1; + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 72]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set parent target base to 0 + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "zero parent target base scenario should succeed" + ); + + // With zero parent target base, derivative should also be 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "zero parent target base: derivative target base should be 0" + ); + } + + #[test] + fn test_mixed_depegged_and_valid_derivatives() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 949_999, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // First derivative - depegged + let derivative1_index = parent_index + 1; + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 300_000, // 30% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Second derivative - valid + let derivative2_index = parent_index + 2; + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 198 * PRICE_PRECISION_I64, // $198 (above 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 400_000, // 40% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + ], + true, + ) + .unwrap(); + + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::Pyth, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + ], + true, + ) + .unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 3, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 96]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 5_000_000_000_000i64; + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative1_index, derivative2_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "mixed depegged and valid derivatives scenario should succeed" + ); + + // First derivative should be depegged (target base = 0) + let derivative1_target_base = constituent_target_base + .get(derivative1_index as u32) + .target_base; + assert_eq!( + derivative1_target_base, 0, + "mixed scenario: depegged derivative should have target base 0" + ); + + // Second derivative should have positive target base + let derivative2_target_base = constituent_target_base + .get(derivative2_index as u32) + .target_base; + assert!( + derivative2_target_base > 0, + "mixed scenario: valid derivative should have positive target base" + ); + + // Parent should be scaled down by only the valid derivative's weight (40%) + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + let expected_parent_target_base = 5_000_000_000_000i64 * (1_000_000 - 400_000) / 1_000_000; + assert_eq!( + parent_target_base, expected_parent_target_base, + "mixed scenario: parent should be scaled by valid derivative weight only" + ); + } +} diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index a9c9724757..cfcce64be2 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -1,3 +1,4 @@ +pub mod constituent_map; pub mod events; pub mod fill_mode; pub mod fulfillment; @@ -6,6 +7,7 @@ pub mod high_leverage_mode_config; pub mod if_rebalance_config; pub mod insurance_fund_stake; pub mod load_ref; +pub mod lp_pool; pub mod margin_calculation; pub mod oracle; pub mod oracle_map; @@ -25,3 +27,4 @@ pub mod state; pub mod traits; pub mod user; pub mod user_map; +pub mod zero_copy; diff --git a/programs/drift/src/state/oracle.rs b/programs/drift/src/state/oracle.rs index b1b328dba7..bb4e024639 100644 --- a/programs/drift/src/state/oracle.rs +++ b/programs/drift/src/state/oracle.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +use bytemuck::{Pod, Zeroable}; use std::cell::Ref; +use std::convert::TryFrom; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; @@ -169,6 +171,55 @@ impl OracleSource { } } +impl TryFrom for OracleSource { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleSource::Pyth), + 1 => Ok(OracleSource::Switchboard), + 2 => Ok(OracleSource::QuoteAsset), + 3 => Ok(OracleSource::Pyth1K), + 4 => Ok(OracleSource::Pyth1M), + 5 => Ok(OracleSource::PythStableCoin), + 6 => Ok(OracleSource::Prelaunch), + 7 => Ok(OracleSource::PythPull), + 8 => Ok(OracleSource::Pyth1KPull), + 9 => Ok(OracleSource::Pyth1MPull), + 10 => Ok(OracleSource::PythStableCoinPull), + 11 => Ok(OracleSource::SwitchboardOnDemand), + 12 => Ok(OracleSource::PythLazer), + 13 => Ok(OracleSource::PythLazer1K), + 14 => Ok(OracleSource::PythLazer1M), + 15 => Ok(OracleSource::PythLazerStableCoin), + _ => Err(ErrorCode::InvalidOracle), + } + } +} + +impl From for u8 { + fn from(src: OracleSource) -> u8 { + match src { + OracleSource::Pyth => 0, + OracleSource::Switchboard => 1, + OracleSource::QuoteAsset => 2, + OracleSource::Pyth1K => 3, + OracleSource::Pyth1M => 4, + OracleSource::PythStableCoin => 5, + OracleSource::Prelaunch => 6, + OracleSource::PythPull => 7, + OracleSource::Pyth1KPull => 8, + OracleSource::Pyth1MPull => 9, + OracleSource::PythStableCoinPull => 10, + OracleSource::SwitchboardOnDemand => 11, + OracleSource::PythLazer => 12, + OracleSource::PythLazer1K => 13, + OracleSource::PythLazer1M => 14, + OracleSource::PythLazerStableCoin => 15, + } + } +} + #[derive(Default, Clone, Copy, Debug)] pub struct OraclePriceData { pub price: i64, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 8983162b7b..b061e8a7af 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,12 +1,16 @@ +use crate::math::spot_balance::get_token_amount; use crate::state::pyth_lazer_oracle::PythLazerOracle; +use crate::state::zero_copy::{AccountZeroCopy, AccountZeroCopyMut}; +use crate::{impl_zero_copy_loader, validate}; use anchor_lang::prelude::*; use crate::state::state::State; use std::cmp::max; +use std::convert::TryFrom; use crate::controller::position::{PositionDelta, PositionDirection}; use crate::error::{DriftResult, ErrorCode}; -use crate::math::amm; +use crate::math::amm::{self, calculate_net_user_pnl}; use crate::math::casting::Cast; #[cfg(test)] use crate::math::constants::{ @@ -35,7 +39,7 @@ use crate::state::oracle::{ get_prelaunch_price, get_sb_on_demand_price, get_switchboard_price, HistoricalOracleData, OracleSource, }; -use crate::state::spot_market::{AssetTier, SpotBalance, SpotBalanceType}; +use crate::state::spot_market::{AssetTier, SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::{MarketIndexOffset, Size}; use borsh::{BorshDeserialize, BorshSerialize}; @@ -45,6 +49,7 @@ use static_assertions::const_assert_eq; use super::oracle_map::OracleIdentifier; use super::protected_maker_mode_config::ProtectedMakerParams; +use super::zero_copy::HasLen; #[cfg(test)] mod tests; @@ -251,7 +256,9 @@ pub struct PerpMarket { pub high_leverage_margin_ratio_maintenance: u16, pub protected_maker_limit_price_divisor: u8, pub protected_maker_dynamic_divisor: u8, - pub padding: [u8; 36], + pub lp_fee_transfer_scalar: u8, + pub lp_status: u8, + pub padding: [u8; 34], } impl Default for PerpMarket { @@ -293,7 +300,9 @@ impl Default for PerpMarket { high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding: [0; 36], + lp_fee_transfer_scalar: 1, + lp_status: 0, + padding: [0; 34], } } } @@ -1702,3 +1711,218 @@ impl AMM { } } } + +pub const AMM_POSITIONS_CACHE: &str = "amm_positions_cache"; + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmCache { + pub bump: u8, + _padding: [u8; 3], + pub cache: Vec, +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +#[repr(C)] +pub struct CacheInfo { + pub last_fee_pool_token_amount: u128, + pub last_net_pnl_pool_token_amount: i128, + /// BASE PRECISION + pub position: i64, + pub slot: u64, + pub max_confidence_interval_multiplier: u64, + pub last_oracle_price_twap: i64, + pub last_settle_amount: u64, + pub last_settle_ts: i64, + pub quote_owed_from_lp_pool: i64, + pub oracle_price: i64, + pub oracle_confidence: u64, + pub oracle_delay: i64, + pub oracle_slot: u64, + pub oracle_source: u8, + pub _padding: [u8; 7], + pub oracle: Pubkey, +} + +impl Size for CacheInfo { + const SIZE: usize = 160 + 8 + 8 + 8; +} + +impl Default for CacheInfo { + fn default() -> Self { + CacheInfo { + position: 0i64, + slot: 0u64, + max_confidence_interval_multiplier: 1u64, + last_oracle_price_twap: 0i64, + oracle_price: 0i64, + oracle_confidence: 0u64, + oracle_delay: 0i64, + oracle_slot: 0u64, + _padding: [0u8; 7], + oracle: Pubkey::default(), + last_fee_pool_token_amount: 0u128, + last_net_pnl_pool_token_amount: 0i128, + last_settle_amount: 0u64, + last_settle_ts: 0i64, + oracle_source: 0u8, + quote_owed_from_lp_pool: 0i64, + } + } +} + +impl CacheInfo { + pub fn get_oracle_source(&self) -> DriftResult { + Ok(OracleSource::try_from(self.oracle_source)?) + } + + pub fn oracle_id(&self) -> DriftResult { + let oracle_source = self.get_oracle_source()?; + Ok((self.oracle, oracle_source)) + } + + pub fn get_last_available_amm_balance(&self) -> DriftResult { + let last_available_balance = self + .last_fee_pool_token_amount + .cast::()? + .safe_add(self.last_net_pnl_pool_token_amount)?; + Ok(last_available_balance) + } +} + +#[zero_copy] +#[derive(Default, Debug)] +#[repr(C)] +pub struct AmmCacheFixed { + pub bump: u8, + _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmCacheFixed { + fn len(&self) -> u32 { + self.len + } +} + +impl AmmCache { + pub fn space(num_markets: usize) -> usize { + 8 + 8 + 4 + num_markets * CacheInfo::SIZE + } + + pub fn validate(&self, state: &State) -> DriftResult<()> { + validate!( + self.cache.len() == state.number_of_markets as usize, + ErrorCode::DefaultError, + "Number of amm positions is different than number of markets" + )?; + Ok(()) + } +} + +impl_zero_copy_loader!(AmmCache, crate::id, AmmCacheFixed, CacheInfo); + +impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> { + pub fn check_settle_staleness(&self, now: i64, threshold_ms: i64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.last_settle_ts < now.saturating_sub(threshold_ms) { + msg!("AMM settle data is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_perp_market_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot < slot.saturating_sub(threshold) { + msg!("Perp market cache info is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_oracle_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.oracle_slot < slot.saturating_sub(threshold) { + msg!( + "Perp market cache info is stale for perp market {}. oracle slot: {}, slot: {}", + i, + cache_info.oracle_slot, + slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, CacheInfo, AmmCacheFixed> { + pub fn update_amount_owed_from_lp_pool( + &mut self, + perp_market: &PerpMarket, + quote_market: &SpotMarket, + ) -> DriftResult<()> { + if perp_market.lp_fee_transfer_scalar == 0 { + msg!( + "lp_fee_transfer_scalar is 0 for perp market {}. not updating quote amount owed in cache", + perp_market.market_index + ); + return Ok(()); + } + + let cached_info = self.get_mut(perp_market.market_index as u32); + + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + "e_market, + perp_market.amm.fee_pool.balance_type(), + )?; + + let net_pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + "e_market, + perp_market.pnl_pool.balance_type(), + )? + .cast::()? + .safe_sub(calculate_net_user_pnl( + &perp_market.amm, + cached_info.oracle_price, + )?)?; + + let amm_amount_available = + net_pnl_pool_token_amount.safe_add(fee_pool_token_amount.cast::()?)?; + + if cached_info.last_net_pnl_pool_token_amount == 0 + && cached_info.last_fee_pool_token_amount == 0 + { + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + return Ok(()); + } + + let amount_to_send = amm_amount_available + .abs_diff(cached_info.get_last_available_amm_balance()?) + .safe_div_ceil(perp_market.lp_fee_transfer_scalar as u128)? + .cast::()?; + + if amm_amount_available < cached_info.get_last_available_amm_balance()? { + cached_info.quote_owed_from_lp_pool = cached_info + .quote_owed_from_lp_pool + .safe_add(amount_to_send.cast::()?)?; + } else { + cached_info.quote_owed_from_lp_pool = cached_info + .quote_owed_from_lp_pool + .safe_sub(amount_to_send.cast::()?)?; + } + + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + + Ok(()) + } +} diff --git a/programs/drift/src/state/zero_copy.rs b/programs/drift/src/state/zero_copy.rs new file mode 100644 index 0000000000..b6a11a383f --- /dev/null +++ b/programs/drift/src/state/zero_copy.rs @@ -0,0 +1,181 @@ +use crate::error::ErrorCode; +use crate::math::safe_unwrap::SafeUnwrap; +use anchor_lang::prelude::{AccountInfo, Pubkey}; +use bytemuck::{from_bytes, from_bytes_mut}; +use bytemuck::{Pod, Zeroable}; +use std::cell::{Ref, RefMut}; +use std::marker::PhantomData; + +use crate::error::DriftResult; +use crate::msg; +use crate::validate; + +pub trait HasLen { + fn len(&self) -> u32; +} + +pub struct AccountZeroCopy<'a, T, F> { + pub fixed: Ref<'a, F>, + pub data: Ref<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopy<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub struct AccountZeroCopyMut<'a, T, F> { + pub fixed: RefMut<'a, F>, + pub data: RefMut<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopyMut<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get_mut(&mut self, index: u32) -> &mut T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes_mut(&mut self.data[start..start + size]) + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub trait ZeroCopyLoader<'a, T, F> { + fn load_zc(&'a self) -> DriftResult>; + fn load_zc_mut(&'a self) -> DriftResult>; +} + +pub fn load_generic<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner {}, program_id: {}", + acct.owner, + program_id, + )?; + + let data = acct.try_borrow_data().safe_unwrap()?; + let (disc, rest) = Ref::map_split(data, |d| d.split_at(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = Ref::map_split(rest, |d| d.split_at(hdr_size)); + let fixed = Ref::map(hdr_bytes, |b| from_bytes::(b)); + Ok(AccountZeroCopy { + fixed, + data: body, + _marker: PhantomData, + }) +} + +pub fn load_generic_mut<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner", + )?; + + let data = acct.try_borrow_mut_data().safe_unwrap()?; + let (disc, rest) = RefMut::map_split(data, |d| d.split_at_mut(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = RefMut::map_split(rest, |d| d.split_at_mut(hdr_size)); + let fixed = RefMut::map(hdr_bytes, |b| from_bytes_mut::(b)); + Ok(AccountZeroCopyMut { + fixed, + data: body, + _marker: PhantomData, + }) +} + +#[macro_export] +macro_rules! impl_zero_copy_loader { + ($Acc:ty, $ID:path, $Fixed:ty, $Elem:ty) => { + impl<'info> crate::state::zero_copy::ZeroCopyLoader<'_, $Elem, $Fixed> + for AccountInfo<'info> + { + fn load_zc<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopy<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + + fn load_zc_mut<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopyMut<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic_mut::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + } + }; +} diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index c99b464690..8a5de1ac2d 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -6,6 +6,7 @@ import { UserAccount, UserStatsAccount, InsuranceFundStake, + ConstituentAccount, } from '../types'; import StrictEventEmitter from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; @@ -249,3 +250,22 @@ export interface HighLeverageModeConfigAccountEvents { update: void; error: (e: Error) => void; } + +export interface ConstituentAccountSubscriber { + eventEmitter: StrictEventEmitter; + isSubscribed: boolean; + + subscribe(constituentAccount?: ConstituentAccount): Promise; + sync(): Promise; + unsubscribe(): Promise; +} + +export interface ConstituentAccountEvents { + onAccountUpdate: ( + account: ConstituentAccount, + pubkey: PublicKey, + slot: number + ) => void; + update: void; + error: (e: Error) => void; +} diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index b93787ef6d..72bd6c24e3 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -394,3 +394,104 @@ export function getIfRebalanceConfigPublicKey( programId )[0]; } + +export function getLpPoolPublicKey( + programId: PublicKey, + nameBuffer: number[] +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('lp_pool')), + Buffer.from(nameBuffer), + ], + programId + )[0]; +} + +export function getLpPoolTokenVaultPublicKey( + programId: PublicKey, + lpPool: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('LP_POOL_TOKEN_VAULT')), + lpPool.toBuffer(), + ], + programId + )[0]; +} +export function getAmmConstituentMappingPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('AMM_MAP')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentTargetBasePublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('constituent_target_base')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getConstituentVaultPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT_VAULT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getAmmCachePublicKey(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from(anchor.utils.bytes.utf8.encode('amm_positions_cache'))], + programId + )[0]; +} + +export function getConstituentCorrelationsPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('constituent_correlations')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 4e17f7be82..c5d9099453 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1,4 +1,7 @@ import { + AddressLookupTableAccount, + Keypair, + LAMPORTS_PER_SOL, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, @@ -15,6 +18,10 @@ import { AssetTier, SpotFulfillmentConfigStatus, IfRebalanceConfigParams, + AddAmmConstituentMappingDatum, + TxParams, + SwapReduceOnly, + InitializeConstituentParams, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -39,9 +46,22 @@ import { getFuelOverflowAccountPublicKey, getTokenProgramForSpotMarket, getIfRebalanceConfigPublicKey, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + getConstituentPublicKey, + getConstituentVaultPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getDriftSignerPublicKey, + getConstituentCorrelationsPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; -import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + createInitializeMint2Instruction, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { DriftClient } from './driftClient'; import { PEG_PRECISION, @@ -57,6 +77,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' @@ -477,7 +502,13 @@ export class AdminClient extends DriftClient { ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; - const initializeMarketIx = await this.getInitializePerpMarketIx( + const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); + const ammCacheAccount = await this.connection.getAccountInfo( + ammCachePublicKey + ); + const mustInitializeAmmCache = ammCacheAccount?.data == null; + + const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, baseAssetReserve, @@ -503,9 +534,10 @@ export class AdminClient extends DriftClient { concentrationCoefScale, curveUpdateIntensity, ammJitIntensity, - name + name, + mustInitializeAmmCache ); - const tx = await this.buildTransaction(initializeMarketIx); + const tx = await this.buildTransaction(initializeMarketIxs); const { txSig } = await this.sendTransaction(tx, [], this.opts); @@ -549,15 +581,21 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME - ): Promise { + name = DEFAULT_MARKET_NAME, + includeInitAmmCacheIx = false + ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, marketIndex ); + const ixs: TransactionInstruction[] = []; + if (includeInitAmmCacheIx) { + ixs.push(await this.getInitializeAmmCacheIx()); + } + const nameBuffer = encodeName(name); - return await this.program.instruction.initializePerpMarket( + const initPerpIx = await this.program.instruction.initializePerpMarket( marketIndex, baseAssetReserve, quoteAssetReserve, @@ -591,11 +629,75 @@ export class AdminClient extends DriftClient { : this.wallet.publicKey, oracle: priceOracle, perpMarket: perpMarketPublicKey, + ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, } ); + ixs.push(initPerpIx); + return ixs; + } + + public async initializeAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getInitializeAmmCacheIx(); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getInitializeAmmCacheIx(): Promise { + return await this.program.instruction.initializeAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async updateInitAmmCacheInfo( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getUpdateInitAmmCacheInfoIx( + perpMarketIndexes + ); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateInitAmmCacheInfoIx( + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], + }); + return await this.program.instruction.updateInitAmmCacheInfo({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); } public async initializePredictionMarket( @@ -869,6 +971,41 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketLpPoolStatus( + perpMarketIndex: number, + lpStatus: number + ) { + const updatePerpMarketLpPoolStatusIx = + await this.getUpdatePerpMarketLpPoolStatusIx(perpMarketIndex, lpStatus); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolStatusIx( + perpMarketIndex: number, + lpStatus: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolStatus( + lpStatus, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + } + ); + } + public async moveAmmToPrice( perpMarketIndex: number, targetPrice: BN @@ -1059,6 +1196,13 @@ export class AdminClient extends DriftClient { sourceVault: PublicKey ): Promise { const spotMarket = this.getQuoteSpotMarketAccount(); + const remainingAccounts = [ + { + pubkey: spotMarket.mint, + isWritable: false, + isSigner: false, + }, + ]; return await this.program.instruction.depositIntoPerpMarketFeePool(amount, { accounts: { @@ -1076,6 +1220,7 @@ export class AdminClient extends DriftClient { spotMarketVault: spotMarket.vault, tokenProgram: TOKEN_PROGRAM_ID, }, + remainingAccounts, }); } @@ -2263,6 +2408,7 @@ export class AdminClient extends DriftClient { ), oracle: oracle, oldOracle: this.getPerpMarketAccount(perpMarketIndex).amm.oracle, + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -3073,6 +3219,7 @@ export class AdminClient extends DriftClient { this.program.programId, perpMarketIndex ), + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -4537,4 +4684,905 @@ export class AdminClient extends DriftClient { }, }); } + + public async initializeLpPool( + name: string, + minMintFee: BN, + maxMintFee: BN, + revenueRebalancePeriod: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair + ): Promise { + const ixs = await this.getInitializeLpPoolIx( + name, + minMintFee, + maxMintFee, + revenueRebalancePeriod, + maxAum, + maxSettleQuoteAmountPerMarket, + mint + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, [mint]); + return txSig; + } + + public async getInitializeLpPoolIx( + name: string, + minMintFee: BN, + maxMintFee: BN, + revenueRebalancePeriod: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, encodeName(name)); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const lamports = + await this.program.provider.connection.getMinimumBalanceForRentExemption( + MINT_SIZE + ); + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: this.wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: Math.min(0.05 * LAMPORTS_PER_SOL, lamports), // should be 0.0014616 ? but bankrun returns 10 SOL + programId: TOKEN_PROGRAM_ID, + }); + const createMintIx = createInitializeMint2Instruction( + mint.publicKey, + 6, + this.getSignerPublicKey(), + null, + TOKEN_PROGRAM_ID + ); + + return [ + createMintAccountIx, + createMintIx, + this.program.instruction.initializeLpPool( + encodeName(name), + minMintFee, + maxMintFee, + revenueRebalancePeriod, + maxAum, + maxSettleQuoteAmountPerMarket, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + ammConstituentMapping, + constituentTargetBase, + mint: mint.publicKey, + state: await this.getStatePublicKey(), + driftSigner: this.getSignerPublicKey(), + tokenProgram: TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }, + signers: [mint], + } + ), + ]; + } + + public async initializeConstituent( + lpPoolName: number[], + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const ixs = await this.getInitializeConstituentIx( + lpPoolName, + initializeConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getInitializeConstituentIx( + lpPoolName: number[], + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const spotMarketIndex = initializeConstituentParams.spotMarketIndex; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + const constituent = getConstituentPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ); + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + + return [ + this.program.instruction.initializeConstituent( + spotMarketIndex, + initializeConstituentParams.decimals, + initializeConstituentParams.maxWeightDeviation, + initializeConstituentParams.swapFeeMin, + initializeConstituentParams.swapFeeMax, + initializeConstituentParams.maxBorrowTokenAmount, + initializeConstituentParams.oracleStalenessThreshold, + initializeConstituentParams.costToTrade, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.constituentDerivativeIndex + : null, + initializeConstituentParams.constituentDerivativeDepegThreshold != null + ? initializeConstituentParams.constituentDerivativeDepegThreshold + : ZERO, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.derivativeWeight + : ZERO, + initializeConstituentParams.volatility != null + ? initializeConstituentParams.volatility + : 10, + initializeConstituentParams.gammaExecution != null + ? initializeConstituentParams.gammaExecution + : 2, + initializeConstituentParams.gammaInventory != null + ? initializeConstituentParams.gammaInventory + : 2, + initializeConstituentParams.xi != null + ? initializeConstituentParams.xi + : 2, + initializeConstituentParams.constituentCorrelations, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentTargetBase, + constituent, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + spotMarketMint: spotMarketAccount.mint, + constituentVault: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + driftSigner: this.getSignerPublicKey(), + tokenProgram: TOKEN_PROGRAM_ID, + }, + signers: [], + } + ), + ]; + } + + public async updateConstituentParams( + lpPoolName: number[], + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + costToTradeBps?: number; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: number; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const ixs = await this.getUpdateConstituentParamsIx( + lpPoolName, + constituentPublicKey, + updateConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentParamsIx( + lpPoolName: number[], + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: number; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return [ + this.program.instruction.updateConstituentParams( + Object.assign( + { + maxWeightDeviation: null, + swapFeeMin: null, + swapFeeMax: null, + maxBorrowTokenAmount: null, + oracleStalenessThreshold: null, + costToTradeBps: null, + stablecoinWeight: null, + derivativeWeight: null, + constituentDerivativeIndex: null, + volatility: null, + gammaExecution: null, + gammaInventory: null, + xi: null, + }, + updateConstituentParams + ), + { + accounts: { + admin: this.wallet.publicKey, + constituent: constituentPublicKey, + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ), + }, + signers: [], + } + ), + ]; + } + + public async updateLpPoolParams( + lpPoolName: number[], + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + } + ): Promise { + const ixs = await this.getUpdateLpPoolParamsIx( + lpPoolName, + updateLpPoolParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateLpPoolParamsIx( + lpPoolName: number[], + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return [ + this.program.instruction.updateLpPoolParams( + Object.assign( + { + maxSettleQuoteAmount: null, + }, + updateLpPoolParams + ), + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + lpPool, + }, + signers: [], + } + ), + ]; + } + + public async addAmmConstituentMappingData( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getAddAmmConstituentMappingDataIx( + lpPoolName, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getAddAmmConstituentMappingDataIx( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.addAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + constituentTargetBase, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateAmmConstituentMappingData( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getUpdateAmmConstituentMappingDataIx( + lpPoolName, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateAmmConstituentMappingDataIx( + lpPoolName: number[], + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.updateAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async removeAmmConstituentMappingData( + lpPoolName: number[], + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const ixs = await this.getRemoveAmmConstituentMappingDataIx( + lpPoolName, + perpMarketIndex, + constituentIndex + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getRemoveAmmConstituentMappingDataIx( + lpPoolName: number[], + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + + return [ + this.program.instruction.removeAmmConstituentMappingData( + perpMarketIndex, + constituentIndex, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateConstituentCorrelationData( + lpPoolName: number[], + index1: number, + index2: number, + correlation: BN + ): Promise { + const ixs = await this.getUpdateConstituentCorrelationDataIx( + lpPoolName, + index1, + index2, + correlation + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentCorrelationDataIx( + lpPoolName: number[], + index1: number, + index2: number, + correlation: BN + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return [ + this.program.instruction.updateConstituentCorrelationData( + index1, + index2, + correlation, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + /** + * 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 }; + } + + public async depositWithdrawToProgramVault( + lpPoolName: number[], + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise { + const { depositIx, withdrawIx } = + await this.getDepositWithdrawToProgramVaultIxs( + lpPoolName, + depositMarketIndex, + borrowMarketIndex, + amountToDeposit, + amountToBorrow + ); + + const tx = await this.buildTransaction([depositIx, withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositWithdrawToProgramVaultIxs( + lpPoolName: number[], + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise<{ + depositIx: TransactionInstruction; + withdrawIx: TransactionInstruction; + }> { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const depositSpotMarket = this.getSpotMarketAccount(depositMarketIndex); + const withdrawSpotMarket = this.getSpotMarketAccount(borrowMarketIndex); + + const depositTokenProgram = + this.getTokenProgramForSpotMarket(depositSpotMarket); + const withdrawTokenProgram = + this.getTokenProgramForSpotMarket(withdrawSpotMarket); + + const depositConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositIx = this.program.instruction.depositToProgramVault( + amountToDeposit, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: depositConstituent, + constituentTokenAccount: depositConstituentTokenAccount, + spotMarket: depositSpotMarket.pubkey, + spotMarketVault: depositSpotMarket.vault, + tokenProgram: depositTokenProgram, + mint: depositSpotMarket.mint, + driftSigner: getDriftSignerPublicKey(this.program.programId), + oracle: depositSpotMarket.oracle, + }, + } + ); + + const withdrawIx = this.program.instruction.withdrawFromProgramVault( + amountToBorrow, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: withdrawConstituent, + constituentTokenAccount: withdrawConstituentTokenAccount, + spotMarket: withdrawSpotMarket.pubkey, + spotMarketVault: withdrawSpotMarket.vault, + tokenProgram: withdrawTokenProgram, + mint: withdrawSpotMarket.mint, + driftSigner: getDriftSignerPublicKey(this.program.programId), + oracle: withdrawSpotMarket.oracle, + }, + } + ); + + return { depositIx, withdrawIx }; + } + + public async depositToProgramVault( + lpPoolName: number[], + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const { depositIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolName, + depositMarketIndex, + depositMarketIndex, + amountToDeposit, + new BN(0) + ); + + const tx = await this.buildTransaction([depositIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async withdrawFromProgramVault( + lpPoolName: number[], + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const { withdrawIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolName, + borrowMarketIndex, + borrowMarketIndex, + new BN(0), + amountToWithdraw + ); + + const tx = await this.buildTransaction([withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } } diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts new file mode 100644 index 0000000000..a8bd476fe5 --- /dev/null +++ b/sdk/src/constituentMap/constituentMap.ts @@ -0,0 +1,245 @@ +import { + Commitment, + MemcmpFilter, + PublicKey, + RpcResponseAndContext, +} from '@solana/web3.js'; +import { ConstituentAccountSubscriber, DataAndSlot } from '../accounts/types'; +import { ConstituentAccount } from '../types'; +import { PollingConstituentAccountSubscriber } from './pollingConstituentAccountSubscriber'; +import { WebSocketConstituentAccountSubscriber } from './webSocketConstituentAccountSubscriber'; +import { DriftClient } from '../driftClient'; +import { getConstituentFilter } from '../memcmp'; +import { ZSTDDecoder } from 'zstddec'; + +const MAX_CONSTITUENT_SIZE_BYTES = 272; // TODO: update this when account is finalized + +export type ConstituentMapConfig = { + driftClient: DriftClient; + subscriptionConfig: + | { + type: 'polling'; + frequency: number; + commitment?: Commitment; + } + | { + type: 'websocket'; + resubTimeoutMs?: number; + logResubMessages?: boolean; + commitment?: Commitment; + }; + // potentially use these to filter Constituent accounts + additionalFilters?: MemcmpFilter[]; +}; + +export interface ConstituentMapInterface { + subscribe(): Promise; + unsubscribe(): Promise; + has(key: string): boolean; + get(key: string): ConstituentAccount | undefined; + getWithSlot(key: string): DataAndSlot | undefined; + mustGet(key: string): Promise; + mustGetWithSlot(key: string): Promise>; +} + +export class ConstituentMap implements ConstituentMapInterface { + private driftClient: DriftClient; + private constituentMap = new Map>(); + private constituentAccountSubscriber: ConstituentAccountSubscriber; + private additionalFilters?: MemcmpFilter[]; + private commitment?: Commitment; + + constructor(config: ConstituentMapConfig) { + this.driftClient = config.driftClient; + this.additionalFilters = config.additionalFilters; + this.commitment = config.subscriptionConfig.commitment; + + if (config.subscriptionConfig.type === 'polling') { + this.constituentAccountSubscriber = + new PollingConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.frequency, + config.subscriptionConfig.commitment, + config.additionalFilters + ); + } else if (config.subscriptionConfig.type === 'websocket') { + this.constituentAccountSubscriber = + new WebSocketConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.resubTimeoutMs, + config.subscriptionConfig.commitment, + config.additionalFilters + ); + } + + // Listen for account updates from the subscriber + this.constituentAccountSubscriber.eventEmitter.on( + 'onAccountUpdate', + (account: ConstituentAccount, pubkey: PublicKey, slot: number) => { + this.updateConstituentAccount(pubkey.toString(), account, slot); + } + ); + } + + private getFilters(): MemcmpFilter[] { + const filters = [getConstituentFilter()]; + if (this.additionalFilters) { + filters.push(...this.additionalFilters); + } + return filters; + } + + private decode(name: string, buffer: Buffer): ConstituentAccount { + return this.driftClient.program.account.constituent.coder.accounts.decodeUnchecked( + name, + buffer + ); + } + + public async sync(): Promise { + try { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.commitment, + filters: this.getFilters(), + encoding: 'base64+zstd', + withContext: true, + }, + ]; + + // @ts-ignore + const rpcJSONResponse: any = await this.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ pubkey: PublicKey; account: { data: [string, string] } }> + > = rpcJSONResponse.result; + const slot = rpcResponseAndContext.context.slot; + + const promises = rpcResponseAndContext.value.map( + async (programAccount) => { + const compressedUserData = Buffer.from( + programAccount.account.data[0], + 'base64' + ); + const decoder = new ZSTDDecoder(); + await decoder.init(); + const buffer = Buffer.from( + decoder.decode(compressedUserData, MAX_CONSTITUENT_SIZE_BYTES) + ); + const key = programAccount.pubkey.toString(); + const currAccountWithSlot = this.getWithSlot(key); + + if (currAccountWithSlot) { + if (slot >= currAccountWithSlot.slot) { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } else { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } + ); + await Promise.all(promises); + } catch (error) { + console.log(`ConstituentMap.sync() error: ${error.message}`); + } + } + + public async subscribe(): Promise { + await this.constituentAccountSubscriber.subscribe(); + } + + public async unsubscribe(): Promise { + await this.constituentAccountSubscriber.unsubscribe(); + this.constituentMap.clear(); + } + + public has(key: string): boolean { + return this.constituentMap.has(key); + } + + public get(key: string): ConstituentAccount | undefined { + return this.constituentMap.get(key)?.data; + } + + public getWithSlot(key: string): DataAndSlot | undefined { + return this.constituentMap.get(key); + } + + public async mustGet(key: string): Promise { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result.data; + } + + public async mustGetWithSlot( + key: string + ): Promise> { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result; + } + + public size(): number { + return this.constituentMap.size; + } + + public *values(): IterableIterator { + for (const dataAndSlot of this.constituentMap.values()) { + yield dataAndSlot.data; + } + } + + public valuesWithSlot(): IterableIterator> { + return this.constituentMap.values(); + } + + public *entries(): IterableIterator<[string, ConstituentAccount]> { + for (const [key, dataAndSlot] of this.constituentMap.entries()) { + yield [key, dataAndSlot.data]; + } + } + + public entriesWithSlot(): IterableIterator< + [string, DataAndSlot] + > { + return this.constituentMap.entries(); + } + + public updateConstituentAccount( + key: string, + constituentAccount: ConstituentAccount, + slot: number + ): void { + const existingData = this.getWithSlot(key); + if (existingData) { + if (slot >= existingData.slot) { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + } else { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + } +} diff --git a/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..e50b34df4b --- /dev/null +++ b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts @@ -0,0 +1,97 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, MemcmpFilter } from '@solana/web3.js'; +import { ConstituentMap } from './constituentMap'; + +export class PollingConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + program: Program; + frequency: number; + commitment?: Commitment; + additionalFilters?: MemcmpFilter[]; + eventEmitter: StrictEventEmitter; + + intervalId?: NodeJS.Timeout; + constituentMap: ConstituentMap; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + frequency: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.frequency = frequency; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + this.eventEmitter = new EventEmitter(); + } + + async subscribe(): Promise { + if (this.isSubscribed || this.frequency <= 0) { + return true; + } + + const executeSync = async () => { + await this.sync(); + this.intervalId = setTimeout(executeSync, this.frequency); + }; + + // Initial sync + await this.sync(); + + // Start polling + this.intervalId = setTimeout(executeSync, this.frequency); + + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `PollingConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + if (this.intervalId) { + clearTimeout(this.intervalId); + this.intervalId = undefined; + } + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } + + didSubscriptionSucceed(): boolean { + return this.isSubscribed; + } +} diff --git a/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..816acd6211 --- /dev/null +++ b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts @@ -0,0 +1,112 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { ConstituentAccount } from '../types'; +import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; +import { getConstituentFilter } from '../memcmp'; +import { ConstituentMap } from './constituentMap'; + +export class WebSocketConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + resubTimeoutMs?: number; + commitment?: Commitment; + program: Program; + eventEmitter: StrictEventEmitter; + + constituentDataAccountSubscriber: WebSocketProgramAccountSubscriber; + constituentMap: ConstituentMap; + private additionalFilters?: MemcmpFilter[]; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + resubTimeoutMs?: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.eventEmitter = new EventEmitter(); + this.resubTimeoutMs = resubTimeoutMs; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + } + + async subscribe(): Promise { + if (this.isSubscribed) { + return true; + } + this.constituentDataAccountSubscriber = + new WebSocketProgramAccountSubscriber( + 'LpPoolConstituent', + 'Constituent', + this.program, + this.program.account.constituent.coder.accounts.decode.bind( + this.program.account.constituent.coder.accounts + ), + { + filters: [getConstituentFilter(), ...(this.additionalFilters || [])], + commitment: this.commitment, + } + ); + + await this.constituentDataAccountSubscriber.subscribe( + (accountId: PublicKey, account: ConstituentAccount, context: Context) => { + this.constituentMap.updateConstituentAccount( + accountId.toBase58(), + account, + context.slot + ); + this.eventEmitter.emit( + 'onAccountUpdate', + account, + accountId, + context.slot + ); + } + ); + + this.eventEmitter.emit('update'); + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `WebSocketConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + await Promise.all([this.constituentDataAccountSubscriber.unsubscribe()]); + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index b77cf253a6..cb83be02bf 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -58,6 +58,8 @@ import { UserStatsAccount, ProtectedMakerModeConfig, SignedMsgOrderParamsDelegateMessage, + LPPoolAccount, + ConstituentAccount, } from './types'; import driftIDL from './idl/drift.json'; @@ -105,6 +107,14 @@ import { getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, + getConstituentTargetBasePublicKey, + getAmmConstituentMappingPublicKey, + getLpPoolPublicKey, + getConstituentPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getConstituentVaultPublicKey, + getConstituentCorrelationsPublicKey, } from './addresses/pda'; import { DataAndSlot, @@ -2443,17 +2453,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 ); } @@ -9745,6 +9757,531 @@ export class DriftClient { return txSig; } + public async updateLpConstituentTargetBase( + lpPoolName: number[], + constituents: PublicKey[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpConstituentTargetBaseIx(lpPoolName, constituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpConstituentTargetBaseIx( + lpPoolName: number[], + constituents: PublicKey[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const ammCache = getAmmCachePublicKey(this.program.programId); + + const remainingAccounts = constituents.map((constituent) => { + return { + isWritable: false, + isSigner: false, + pubkey: constituent, + }; + }); + + return this.program.instruction.updateLpConstituentTargetBase({ + accounts: { + keeper: this.wallet.publicKey, + lpPool, + ammConstituentMapping: ammConstituentMappingPublicKey, + constituentTargetBase, + state: await this.getStatePublicKey(), + ammCache, + }, + remainingAccounts, + }); + } + + public async updateLpPoolAum( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexOfConstituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: spotMarketIndexOfConstituents, + }); + remainingAccounts.push( + ...spotMarketIndexOfConstituents.map((index) => { + return { + pubkey: getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + index + ), + isSigner: false, + isWritable: true, + }; + }) + ); + return this.program.instruction.updateLpPoolAum({ + accounts: { + keeper: this.wallet.publicKey, + lpPool: lpPool.pubkey, + state: await this.getStatePublicKey(), + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async updateAmmCache( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateAmmCacheIx(perpMarketIndexes), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateAmmCacheIx( + perpMarketIndexes: number[] + ): Promise { + if (perpMarketIndexes.length > 50) { + throw new Error('Cant update more than 50 markets at once'); + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + }); + + return this.program.instruction.updateAmmCache({ + accounts: { + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: this.getSpotMarketAccount(0).pubkey, + }, + remainingAccounts, + }); + } + + public async updateConstituentOracleInfo( + constituent: ConstituentAccount + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateConstituentOracleInfoIx(constituent), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateConstituentOracleInfoIx( + constituent: ConstituentAccount + ): Promise { + const spotMarket = await this.getSpotMarketAccount( + constituent.spotMarketIndex + ); + return this.program.instruction.updateConstituentOracleInfo({ + accounts: { + keeper: this.wallet.publicKey, + constituent: constituent.pubkey, + state: await this.getStatePublicKey(), + oracle: spotMarket.oracle, + spotMarket: spotMarket.pubkey, + }, + }); + } + + public async lpPoolSwap( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + userInTokenAccount: PublicKey, + userOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + inMarketMint: PublicKey, + outMarketMint: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + userInTokenAccount, + userOutTokenAccount, + inConstituent, + outConstituent, + inMarketMint, + outMarketMint + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolSwapIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + userInTokenAccount: PublicKey, + userOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + inMarketMint: PublicKey, + outMarketMint: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + return this.program.instruction.lpPoolSwap( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + userInTokenAccount, + userOutTokenAccount, + inConstituent, + outConstituent, + inMarketMint, + outMarketMint, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async lpPoolAddLiquidity({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const userInTokenAccount = await this.getAssociatedTokenAccount( + inMarketIndex + ); + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getAssociatedTokenAddress( + lpMint, + this.wallet.publicKey, + true + ); + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.lpPoolAddLiquidity( + inMarketIndex, + inAmount, + minMintAmount, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + userInTokenAccount, + constituentInTokenAccount, + userLpTokenAccount, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async lpPoolRemoveLiquidity({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const userOutTokenAccount = await this.getAssociatedTokenAccount( + outMarketIndex + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getAssociatedTokenAddress( + lpMint, + this.wallet.publicKey, + true + ); + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.lpPoolRemoveLiquidity( + outMarketIndex, + lpToBurn, + minAmountOut, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + userOutTokenAccount, + constituentOutTokenAccount, + userLpTokenAccount, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + async settlePerpToLpPool( + lpPoolName: number[], + perpMarketIndexes: number[] + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getSettlePerpToLpPoolIx(lpPoolName, perpMarketIndexes), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getSettlePerpToLpPoolIx( + lpPoolName: number[], + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = []; + remainingAccounts.push( + ...perpMarketIndexes.map((index) => { + return { + pubkey: this.getPerpMarketAccount(index).pubkey, + isSigner: false, + isWritable: true, + }; + }) + ); + const quoteSpotMarketAccount = this.getQuoteSpotMarketAccount(); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + return this.program.instruction.settlePerpToLpPool({ + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: quoteSpotMarketAccount.pubkey, + constituent: getConstituentPublicKey(this.program.programId, lpPool, 0), + constituentQuoteTokenAccount: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + 0 + ), + lpPool, + quoteTokenVault: quoteSpotMarketAccount.vault, + tokenProgram: this.getTokenProgramForSpotMarket(quoteSpotMarketAccount), + mint: quoteSpotMarketAccount.mint, + }, + remainingAccounts, + }); + } + + /** + * Below here are the transaction sending functions + */ + private handleSignedTransaction(signedTxs: SignedTxData[]) { if (this.enableMetricsEvents && this.metricsEventEmitter) { this.metricsEventEmitter.emit('txSigned', signedTxs); diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index f1b27e637c..03d7c52140 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4407,6 +4407,11 @@ "isMut": true, "isSigner": false }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, { "name": "oracle", "isMut": false, @@ -4535,6 +4540,58 @@ } ] }, + { + "name": "initializeAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateInitAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializePredictionMarket", "accounts": [ @@ -4748,6 +4805,32 @@ } ] }, + { + "name": "updatePerpMarketLpPoolStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpStatus", + "type": "u8" + } + ] + }, { "name": "settleExpiredMarketPoolsToRevenuePool", "accounts": [ @@ -5837,6 +5920,11 @@ "name": "perpMarket", "isMut": true, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -6255,6 +6343,11 @@ "name": "oldOracle", "isMut": false, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -7305,6 +7398,102 @@ } ] }, + { + "name": "initializeLpPool", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxMintFee", + "type": "i64" + }, + { + "name": "revenueRebalancePeriod", + "type": "u64" + }, + { + "name": "maxAum", + "type": "u128" + }, + { + "name": "maxSettleQuoteAmountPerMarket", + "type": "u64" + } + ] + }, { "name": "updateHighLeverageModeConfig", "accounts": [ @@ -7523,96 +7712,1207 @@ } } ] - } - ], - "accounts": [ - { - "name": "OpenbookV2FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "openbookV2ProgramId", - "type": "publicKey" - }, - { - "name": "openbookV2Market", - "type": "publicKey" - }, - { - "name": "openbookV2MarketAuthority", - "type": "publicKey" - }, - { - "name": "openbookV2EventHeap", - "type": "publicKey" - }, - { - "name": "openbookV2Bids", - "type": "publicKey" - }, - { - "name": "openbookV2Asks", - "type": "publicKey" - }, - { - "name": "openbookV2BaseVault", - "type": "publicKey" - }, - { - "name": "openbookV2QuoteVault", - "type": "publicKey" - }, - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } - }, - { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } - } - ] - } }, { - "name": "PhoenixV1FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "phoenixProgramId", - "type": "publicKey" - }, - { - "name": "phoenixLogAuthority", - "type": "publicKey" - }, - { + "name": "initializeConstituent", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "maxWeightDeviation", + "type": "i64" + }, + { + "name": "swapFeeMin", + "type": "i64" + }, + { + "name": "swapFeeMax", + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", + "type": "u64" + }, + { + "name": "costToTrade", + "type": "i32" + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "derivativeWeight", + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "newConstituentCorrelations", + "type": { + "vec": "i64" + } + } + ] + }, + { + "name": "updateConstituentParams", + "accounts": [ + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "constituentParams", + "type": { + "defined": "ConstituentParams" + } + } + ] + }, + { + "name": "updateLpPoolParams", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolParams", + "type": { + "defined": "LpPoolParams" + } + } + ] + }, + { + "name": "addAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "updateAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "removeAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentCorrelationData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "index1", + "type": "u16" + }, + { + "name": "index2", + "type": "u16" + }, + { + "name": "correlation", + "type": "i64" + } + ] + }, + { + "name": "updateLpConstituentTargetBase", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammConstituentMapping", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateLpPoolAum", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmmCache", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "lpPoolSwap", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u64" + } + ] + }, + { + "name": "lpPoolAddLiquidity", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + }, + { + "name": "minMintAmount", + "type": "u64" + } + ] + }, + { + "name": "lpPoolRemoveLiquidity", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u128" + } + ] + }, + { + "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" + } + ] + }, + { + "name": "updateConstituentOracleInfo", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositToProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdrawFromProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "settlePerpToLpPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentQuoteTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "openbookV2ProgramId", + "type": "publicKey" + }, + { + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { "name": "phoenixMarket", "type": "publicKey" }, @@ -7629,23 +8929,239 @@ "type": "u16" }, { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, + { + "name": "serumRequestQueue", + "type": "publicKey" + }, + { + "name": "serumEventQueue", + "type": "publicKey" + }, + { + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", + "type": "publicKey" + }, + { + "name": "serumSignerNonce", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "HighLeverageModeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxUsers", + "type": "u32" + }, + { + "name": "currentUsers", + "type": "u32" + }, + { + "name": "reduceOnly", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 31 + ] + } + } + ] + } + }, + { + "name": "IfRebalanceConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "totalInAmount", + "docs": [ + "total amount to be sold" + ], + "type": "u64" + }, + { + "name": "currentInAmount", + "docs": [ + "amount already sold" + ], + "type": "u64" + }, + { + "name": "currentOutAmount", + "docs": [ + "amount already bought" + ], + "type": "u64" + }, + { + "name": "currentOutAmountTransferred", + "docs": [ + "amount already transferred to revenue pool" + ], + "type": "u64" + }, + { + "name": "currentInAmountSinceLastTransfer", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochStartTs", + "docs": [ + "start time of epoch" + ], + "type": "i64" + }, + { + "name": "epochInAmount", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "docs": [ + "max amount to swap in epoch" + ], + "type": "u64" + }, + { + "name": "epochDuration", + "docs": [ + "duration of epoch" + ], + "type": "i64" + }, + { + "name": "outMarketIndex", + "docs": [ + "market index to sell" + ], + "type": "u16" + }, + { + "name": "inMarketIndex", + "docs": [ + "market index to buy" + ], + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" }, { "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } + "type": "u8" }, { - "name": "padding", + "name": "padding2", "type": { "array": [ "u8", - 4 + 32 ] } } @@ -7653,76 +9169,90 @@ } }, { - "name": "SerumV3FulfillmentConfig", + "name": "InsuranceFundStake", "type": { "kind": "struct", "fields": [ { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "serumProgramId", - "type": "publicKey" - }, - { - "name": "serumMarket", - "type": "publicKey" - }, - { - "name": "serumRequestQueue", + "name": "authority", "type": "publicKey" }, { - "name": "serumEventQueue", - "type": "publicKey" + "name": "ifShares", + "type": "u128" }, { - "name": "serumBids", - "type": "publicKey" + "name": "lastWithdrawRequestShares", + "type": "u128" }, { - "name": "serumAsks", - "type": "publicKey" + "name": "ifBase", + "type": "u128" }, { - "name": "serumBaseVault", - "type": "publicKey" + "name": "lastValidTs", + "type": "i64" }, { - "name": "serumQuoteVault", - "type": "publicKey" + "name": "lastWithdrawRequestValue", + "type": "u64" }, { - "name": "serumOpenOrders", - "type": "publicKey" + "name": "lastWithdrawRequestTs", + "type": "i64" }, { - "name": "serumSignerNonce", - "type": "u64" + "name": "costBasis", + "type": "i64" }, { "name": "marketIndex", "type": "u16" }, { - "name": "fulfillmentType", + "name": "padding", "type": { - "defined": "SpotFulfillmentType" + "array": [ + "u8", + 14 + ] } - }, + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ { - "name": "status", + "name": "whitelistedSigners", "type": { - "defined": "SpotFulfillmentConfigStatus" + "array": [ + "publicKey", + 4 + ] } }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, { "name": "padding", "type": { "array": [ - "u8", - 4 + "u128", + 8 ] } } @@ -7730,138 +9260,311 @@ } }, { - "name": "HighLeverageModeConfig", + "name": "LPPool", "type": { "kind": "struct", "fields": [ { - "name": "maxUsers", - "type": "u32" + "name": "name", + "docs": [ + "name of vault, TODO: check type + size" + ], + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "currentUsers", - "type": "u32" + "name": "pubkey", + "docs": [ + "address of the vault." + ], + "type": "publicKey" }, { - "name": "reduceOnly", + "name": "mint", + "type": "publicKey" + }, + { + "name": "maxAum", + "docs": [ + "The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index)", + "which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0)", + "pub quote_constituent_index: u16,", + "QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this" + ], + "type": "u128" + }, + { + "name": "lastAum", + "docs": [ + "QUOTE_PRECISION: AUM of the vault in USD, updated lazily" + ], + "type": "u128" + }, + { + "name": "lastAumSlot", + "docs": [ + "timestamp of last AUM slot" + ], + "type": "u64" + }, + { + "name": "lastAumTs", + "docs": [ + "timestamp of last AUM update" + ], + "type": "i64" + }, + { + "name": "maxSettleQuoteAmount", + "docs": [ + "Oldest slot of constituent oracles" + ], + "type": "u64" + }, + { + "name": "lastRevenueRebalanceTs", + "docs": [ + "timestamp of last vAMM revenue rebalance" + ], + "type": "u64" + }, + { + "name": "revenueRebalancePeriod", + "type": "u64" + }, + { + "name": "nextMintRedeemId", + "docs": [ + "Every mint/redeem has a monotonically increasing id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "totalFeesReceived", + "docs": [ + "all revenue settles recieved" + ], + "type": "u128" + }, + { + "name": "totalFeesPaid", + "docs": [ + "all revenues paid out" + ], + "type": "u128" + }, + { + "name": "cumulativeUsdcSentToPerpMarkets", + "type": "u128" + }, + { + "name": "cumulativeUsdcReceivedFromPerpMarkets", + "type": "u128" + }, + { + "name": "totalMintRedeemFeesPaid", + "type": "i128" + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxMintFeePremium", + "type": "i64" + }, + { + "name": "constituents", + "type": "u16" + }, + { + "name": "bump", "type": "u8" }, { - "name": "padding", - "type": { - "array": [ - "u8", - 31 - ] - } + "name": "usdcConsituentIndex", + "type": "u16" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "volatility", + "type": "u64" } ] } }, { - "name": "IfRebalanceConfig", + "name": "Constituent", "type": { "kind": "struct", "fields": [ { "name": "pubkey", + "docs": [ + "address of the constituent" + ], "type": "publicKey" }, { - "name": "totalInAmount", + "name": "mint", + "type": "publicKey" + }, + { + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "tokenVault", + "type": "publicKey" + }, + { + "name": "totalSwapFees", "docs": [ - "total amount to be sold" + "total fees received by the constituent. Positive = fees received, Negative = fees paid" ], - "type": "u64" + "type": "i128" }, { - "name": "currentInAmount", + "name": "spotBalance", "docs": [ - "amount already sold" + "spot borrow-lend balance for constituent" + ], + "type": { + "defined": "BLPosition" + } + }, + { + "name": "maxWeightDeviation", + "docs": [ + "max deviation from target_weight allowed for the constituent", + "precision: PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "swapFeeMin", + "docs": [ + "min fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "swapFeeMax", + "docs": [ + "max fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "docs": [ + "Max Borrow amount:", + "precision: token precision" ], "type": "u64" }, { - "name": "currentOutAmount", + "name": "tokenBalance", "docs": [ - "amount already bought" + "ata token balance in token precision" ], "type": "u64" }, { - "name": "currentOutAmountTransferred", - "docs": [ - "amount already transferred to revenue pool" - ], + "name": "lastOraclePrice", + "type": "i64" + }, + { + "name": "lastOracleSlot", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", "type": "u64" }, { - "name": "currentInAmountSinceLastTransfer", - "docs": [ - "amount already bought in epoch" - ], + "name": "flashLoanInitialTokenAmount", "type": "u64" }, { - "name": "epochStartTs", + "name": "nextSwapId", "docs": [ - "start time of epoch" + "Every swap to/from this constituent has a monotonically increasing id. This is the next id to use" ], - "type": "i64" + "type": "u64" }, { - "name": "epochInAmount", + "name": "derivativeWeight", "docs": [ - "amount already bought in epoch" + "percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight" ], "type": "u64" }, { - "name": "epochMaxInAmount", - "docs": [ - "max amount to swap in epoch" - ], + "name": "volatility", "type": "u64" }, { - "name": "epochDuration", - "docs": [ - "duration of epoch" - ], - "type": "i64" + "name": "constituentDerivativeDepegThreshold", + "type": "u64" }, { - "name": "outMarketIndex", + "name": "constituentDerivativeIndex", "docs": [ - "market index to sell" + "The `constituent_index` of the parent constituent. -1 if it is a parent index", + "Example: if in a pool with SOL (parent) and dSOL (derivative),", + "SOL.constituent_index = 1, SOL.constituent_derivative_index = -1,", + "dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1" ], - "type": "u16" + "type": "i16" }, { - "name": "inMarketIndex", - "docs": [ - "market index to buy" - ], + "name": "spotMarketIndex", "type": "u16" }, { - "name": "maxSlippageBps", + "name": "constituentIndex", "type": "u16" }, { - "name": "swapMode", + "name": "decimals", "type": "u8" }, { - "name": "status", + "name": "bump", "type": "u8" }, { - "name": "padding2", + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 32 + 5 ] } } @@ -7869,92 +9572,98 @@ } }, { - "name": "InsuranceFundStake", + "name": "AmmConstituentMapping", "type": { "kind": "struct", "fields": [ { - "name": "authority", + "name": "lpPool", "type": "publicKey" }, { - "name": "ifShares", - "type": "u128" - }, - { - "name": "lastWithdrawRequestShares", - "type": "u128" - }, - { - "name": "ifBase", - "type": "u128" - }, - { - "name": "lastValidTs", - "type": "i64" - }, - { - "name": "lastWithdrawRequestValue", - "type": "u64" - }, - { - "name": "lastWithdrawRequestTs", - "type": "i64" - }, - { - "name": "costBasis", - "type": "i64" - }, - { - "name": "marketIndex", - "type": "u16" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ "u8", - 14 + 3 ] } + }, + { + "name": "weights", + "type": { + "vec": { + "defined": "AmmConstituentDatum" + } + } } ] } }, { - "name": "ProtocolIfSharesTransferConfig", + "name": "ConstituentTargetBase", "type": { "kind": "struct", "fields": [ { - "name": "whitelistedSigners", + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ - "publicKey", - 4 + "u8", + 3 ] } }, { - "name": "maxTransferPerEpoch", - "type": "u128" - }, + "name": "targets", + "type": { + "vec": { + "defined": "TargetsDatum" + } + } + } + ] + } + }, + { + "name": "ConstituentCorrelations", + "type": { + "kind": "struct", + "fields": [ { - "name": "currentEpochTransfer", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "nextEpochTs", - "type": "i64" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 8 + "u8", + 3 ] } + }, + { + "name": "correlations", + "type": { + "vec": "i64" + } } ] } @@ -8276,14 +9985,51 @@ "name": "protectedMakerDynamicDivisor", "type": "u8" }, + { + "name": "lpFeeTransferScalar", + "type": "u8" + }, + { + "name": "lpStatus", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 34 + ] + } + } + ] + } + }, + { + "name": "AmmCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 36 + 3 ] } + }, + { + "name": "cache", + "type": { + "vec": { + "defined": "CacheInfo" + } + } } ] } @@ -9515,86 +11261,200 @@ "kind": "struct", "fields": [ { - "name": "authority", - "docs": [ - "The authority of this overflow account" - ], - "type": "publicKey" + "name": "authority", + "docs": [ + "The authority of this overflow account" + ], + "type": "publicKey" + }, + { + "name": "fuelInsurance", + "type": "u128" + }, + { + "name": "fuelDeposits", + "type": "u128" + }, + { + "name": "fuelBorrows", + "type": "u128" + }, + { + "name": "fuelPositions", + "type": "u128" + }, + { + "name": "fuelTaker", + "type": "u128" + }, + { + "name": "fuelMaker", + "type": "u128" + }, + { + "name": "lastFuelSweepTs", + "type": "u32" + }, + { + "name": "lastResetTs", + "type": "u32" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 6 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } + }, + { + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } + }, + { + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } + }, + { + "name": "excludeTotalLiqFee", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "ConstituentParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxWeightDeviation", + "type": { + "option": "i64" + } + }, + { + "name": "swapFeeMin", + "type": { + "option": "i64" + } }, { - "name": "fuelInsurance", - "type": "u128" + "name": "swapFeeMax", + "type": { + "option": "i64" + } }, { - "name": "fuelDeposits", - "type": "u128" + "name": "maxBorrowTokenAmount", + "type": { + "option": "u64" + } }, { - "name": "fuelBorrows", - "type": "u128" + "name": "oracleStalenessThreshold", + "type": { + "option": "u64" + } }, { - "name": "fuelPositions", - "type": "u128" + "name": "costToTradeBps", + "type": { + "option": "i32" + } }, { - "name": "fuelTaker", - "type": "u128" + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } }, { - "name": "fuelMaker", - "type": "u128" + "name": "derivativeWeight", + "type": { + "option": "u64" + } }, { - "name": "lastFuelSweepTs", - "type": "u32" + "name": "volatility", + "type": { + "option": "u8" + } }, { - "name": "lastResetTs", - "type": "u32" + "name": "gammaExecution", + "type": { + "option": "u8" + } }, { - "name": "padding", + "name": "gammaInventory", "type": { - "array": [ - "u128", - 6 - ] + "option": "u8" + } + }, + { + "name": "xi", + "type": { + "option": "u8" } } ] } - } - ], - "types": [ + }, { - "name": "UpdatePerpMarketSummaryStatsParams", + "name": "LpPoolParams", "type": { "kind": "struct", "fields": [ { - "name": "quoteAssetAmountWithUnsettledLp", + "name": "maxSettleQuoteAmount", "type": { - "option": "i64" + "option": "u64" } - }, + } + ] + } + }, + { + "name": "AddAmmConstituentMappingDatum", + "type": { + "kind": "struct", + "fields": [ { - "name": "netUnsettledFundingPnl", - "type": { - "option": "i64" - } + "name": "constituentIndex", + "type": "u16" }, { - "name": "updateAmmSummaryStats", - "type": { - "option": "bool" - } + "name": "perpMarketIndex", + "type": "u16" }, { - "name": "excludeTotalLiqFee", - "type": { - "option": "bool" - } + "name": "weight", + "type": "i64" } ] } @@ -9689,49 +11549,231 @@ "type": "u128" }, { - "name": "ifFee", - "docs": [ - "precision: token mint precision" - ], - "type": "u64" + "name": "ifFee", + "docs": [ + "precision: token mint precision" + ], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateBorrowForPerpPnlRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "liabilityMarketIndex", + "type": "u16" + }, + { + "name": "liabilityPrice", + "type": "i64" + }, + { + "name": "liabilityTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidatePerpPnlForDepositRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "marketOraclePrice", + "type": "i64" + }, + { + "name": "pnlTransfer", + "type": "u128" + }, + { + "name": "assetMarketIndex", + "type": "u16" + }, + { + "name": "assetPrice", + "type": "i64" + }, + { + "name": "assetTransfer", + "type": "u128" + } + ] + } + }, + { + "name": "PerpBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "pnl", + "type": "i128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "clawbackUser", + "type": { + "option": "publicKey" + } + }, + { + "name": "clawbackUserPayment", + "type": { + "option": "u128" + } + }, + { + "name": "cumulativeFundingRateDelta", + "type": "i128" + } + ] + } + }, + { + "name": "SpotBankruptcyRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "borrowAmount", + "type": "u128" + }, + { + "name": "ifPayment", + "type": "u128" + }, + { + "name": "cumulativeDepositInterestDelta", + "type": "u128" + } + ] + } + }, + { + "name": "IfRebalanceConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalInAmount", + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "type": "u64" + }, + { + "name": "epochDuration", + "type": "i64" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" } ] } }, { - "name": "LiquidateBorrowForPerpPnlRecord", + "name": "BLPosition", "type": { "kind": "struct", "fields": [ { - "name": "perpMarketIndex", - "type": "u16" + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: token precision" + ], + "type": "u128" }, { - "name": "marketOraclePrice", + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], "type": "i64" }, { - "name": "pnlTransfer", - "type": "u128" - }, - { - "name": "liabilityMarketIndex", + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], "type": "u16" }, { - "name": "liabilityPrice", - "type": "i64" + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } }, { - "name": "liabilityTransfer", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 5 + ] + } } ] } }, { - "name": "LiquidatePerpPnlForDepositRecord", + "name": "AmmConstituentDatum", "type": { "kind": "struct", "fields": [ @@ -9740,124 +11782,150 @@ "type": "u16" }, { - "name": "marketOraclePrice", - "type": "i64" + "name": "constituentIndex", + "type": "u16" }, { - "name": "pnlTransfer", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "assetMarketIndex", - "type": "u16" + "name": "lastSlot", + "type": "u64" }, { - "name": "assetPrice", + "name": "weight", + "docs": [ + "PERCENTAGE_PRECISION. The weight this constituent has on the perp market" + ], "type": "i64" - }, - { - "name": "assetTransfer", - "type": "u128" } ] } }, { - "name": "PerpBankruptcyRecord", + "name": "AmmConstituentMappingFixed", "type": { "kind": "struct", "fields": [ { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "pnl", - "type": "i128" - }, - { - "name": "ifPayment", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "clawbackUser", - "type": { - "option": "publicKey" - } + "name": "bump", + "type": "u8" }, { - "name": "clawbackUserPayment", + "name": "pad", "type": { - "option": "u128" + "array": [ + "u8", + 3 + ] } }, { - "name": "cumulativeFundingRateDelta", - "type": "i128" + "name": "len", + "type": "u32" } ] } }, { - "name": "SpotBankruptcyRecord", + "name": "TargetsDatum", "type": { "kind": "struct", "fields": [ { - "name": "marketIndex", - "type": "u16" + "name": "costToTradeBps", + "type": "i32" }, { - "name": "borrowAmount", - "type": "u128" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "ifPayment", - "type": "u128" + "name": "lastSlot", + "type": "u64" }, { - "name": "cumulativeDepositInterestDelta", - "type": "u128" + "name": "targetBase", + "type": "i64" } ] } }, { - "name": "IfRebalanceConfigParams", + "name": "ConstituentTargetBaseFixed", "type": { "kind": "struct", "fields": [ { - "name": "totalInAmount", - "type": "u64" + "name": "lpPool", + "type": "publicKey" }, { - "name": "epochMaxInAmount", - "type": "u64" + "name": "bump", + "type": "u8" }, { - "name": "epochDuration", - "type": "i64" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "outMarketIndex", - "type": "u16" - }, + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" + } + ] + } + }, + { + "name": "ConstituentCorrelationsFixed", + "type": { + "kind": "struct", + "fields": [ { - "name": "inMarketIndex", - "type": "u16" + "name": "lpPool", + "type": "publicKey" }, { - "name": "maxSlippageBps", - "type": "u16" + "name": "bump", + "type": "u8" }, { - "name": "swapMode", - "type": "u8" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "status", - "type": "u8" + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" } ] } @@ -11022,20 +13090,125 @@ "type": "i32" }, { - "name": "ammInventorySpreadAdjustment", - "docs": [ - "signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double)" - ], - "type": "i8" + "name": "ammInventorySpreadAdjustment", + "docs": [ + "signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double)" + ], + "type": "i8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 11 + ] + } + } + ] + } + }, + { + "name": "CacheInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lastFeePoolTokenAmount", + "type": "u128" + }, + { + "name": "lastNetPnlPoolTokenAmount", + "type": "i128" + }, + { + "name": "position", + "docs": [ + "BASE PRECISION" + ], + "type": "i64" + }, + { + "name": "slot", + "type": "u64" + }, + { + "name": "maxConfidenceIntervalMultiplier", + "type": "u64" + }, + { + "name": "lastOraclePriceTwap", + "type": "i64" + }, + { + "name": "lastSettleAmount", + "type": "u64" + }, + { + "name": "lastSettleTs", + "type": "i64" + }, + { + "name": "quoteOwedFromLpPool", + "type": "i64" + }, + { + "name": "oraclePrice", + "type": "i64" + }, + { + "name": "oracleConfidence", + "type": "u64" + }, + { + "name": "oracleDelay", + "type": "i64" + }, + { + "name": "oracleSlot", + "type": "u64" + }, + { + "name": "oracleSource", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "oracle", + "type": "publicKey" + } + ] + } + }, + { + "name": "AmmCacheFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" }, { - "name": "padding", + "name": "pad", "type": { "array": [ "u8", - 11 + 3 ] } + }, + { + "name": "len", + "type": "u32" } ] } @@ -11870,6 +14043,23 @@ ] } }, + { + "name": "SettlementDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ToLpPool" + }, + { + "name": "FromLpPool" + }, + { + "name": "None" + } + ] + } + }, { "name": "MarginRequirementType", "type": { @@ -11950,6 +14140,15 @@ }, { "name": "OracleOrderPrice" + }, + { + "name": "UpdateLpConstituentTargetBase" + }, + { + "name": "UpdateLpPoolAum" + }, + { + "name": "LpPoolSwap" } ] } @@ -12260,6 +14459,26 @@ ] } }, + { + "name": "WeightValidationFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NONE" + }, + { + "name": "EnforceTotalWeight100" + }, + { + "name": "NoNegativeWeights" + }, + { + "name": "NoOverweight" + } + ] + } + }, { "name": "MarginCalculationMode", "type": { @@ -14325,6 +16544,211 @@ "index": false } ] + }, + { + "name": "LPSwapRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "slot", + "type": "u64", + "index": false + }, + { + "name": "authority", + "type": "publicKey", + "index": false + }, + { + "name": "outAmount", + "type": "u128", + "index": false + }, + { + "name": "inAmount", + "type": "u128", + "index": false + }, + { + "name": "outFee", + "type": "i128", + "index": false + }, + { + "name": "inFee", + "type": "i128", + "index": false + }, + { + "name": "outSpotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "inSpotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "outConstituentIndex", + "type": "u16", + "index": false + }, + { + "name": "inConstituentIndex", + "type": "u16", + "index": false + }, + { + "name": "outOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "inOraclePrice", + "type": "i64", + "index": false + }, + { + "name": "lastAum", + "type": "u128", + "index": false + }, + { + "name": "lastAumSlot", + "type": "u64", + "index": false + }, + { + "name": "inMarketCurrentWeight", + "type": "i64", + "index": false + }, + { + "name": "outMarketCurrentWeight", + "type": "i64", + "index": false + }, + { + "name": "inMarketTargetWeight", + "type": "i64", + "index": false + }, + { + "name": "outMarketTargetWeight", + "type": "i64", + "index": false + }, + { + "name": "inSwapId", + "type": "u64", + "index": false + }, + { + "name": "outSwapId", + "type": "u64", + "index": false + } + ] + }, + { + "name": "LPMintRedeemRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "slot", + "type": "u64", + "index": false + }, + { + "name": "authority", + "type": "publicKey", + "index": false + }, + { + "name": "description", + "type": "u8", + "index": false + }, + { + "name": "amount", + "type": "u128", + "index": false + }, + { + "name": "fee", + "type": "i128", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "constituentIndex", + "type": "u16", + "index": false + }, + { + "name": "oraclePrice", + "type": "i64", + "index": false + }, + { + "name": "mint", + "type": "publicKey", + "index": false + }, + { + "name": "lpAmount", + "type": "u64", + "index": false + }, + { + "name": "lpFee", + "type": "i64", + "index": false + }, + { + "name": "lpPrice", + "type": "u128", + "index": false + }, + { + "name": "mintRedeemId", + "type": "u64", + "index": false + }, + { + "name": "lastAum", + "type": "u128", + "index": false + }, + { + "name": "lastAumSlot", + "type": "u64", + "index": false + }, + { + "name": "inMarketCurrentWeight", + "type": "i64", + "index": false + }, + { + "name": "inMarketTargetWeight", + "type": "i64", + "index": false + } + ] } ], "errors": [ @@ -15912,6 +18336,76 @@ "code": 6316, "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" + }, + { + "code": 6317, + "name": "InvalidConstituent", + "msg": "Invalid Constituent" + }, + { + "code": 6318, + "name": "InvalidAmmConstituentMappingArgument", + "msg": "Invalid Amm Constituent Mapping argument" + }, + { + "code": 6319, + "name": "InvalidUpdateConstituentTargetBaseArgument", + "msg": "Invalid update constituent update target weights argument" + }, + { + "code": 6320, + "name": "ConstituentNotFound", + "msg": "Constituent not found" + }, + { + "code": 6321, + "name": "ConstituentCouldNotLoad", + "msg": "Constituent could not load" + }, + { + "code": 6322, + "name": "ConstituentWrongMutability", + "msg": "Constituent wrong mutability" + }, + { + "code": 6323, + "name": "WrongNumberOfConstituents", + "msg": "Wrong number of constituents passed to instruction" + }, + { + "code": 6324, + "name": "OracleTooStaleForLPAUMUpdate", + "msg": "Oracle too stale for LP AUM update" + }, + { + "code": 6325, + "name": "InsufficientConstituentTokenBalance", + "msg": "Insufficient constituent token balance" + }, + { + "code": 6326, + "name": "AMMCacheStale", + "msg": "Amm Cache data too stale" + }, + { + "code": 6327, + "name": "LpPoolAumDelayed", + "msg": "LP Pool AUM not updated recently" + }, + { + "code": 6328, + "name": "ConstituentOracleStale", + "msg": "Constituent oracle is stale" + }, + { + "code": 6329, + "name": "LpInvariantFailed", + "msg": "LP Invariant failed" + }, + { + "code": 6330, + "name": "InvalidConstituentDerivativeWeights", + "msg": "Invalid constituent derivative weights" } ], "metadata": { diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 8cac837f39..0f4ae8ee19 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -25,6 +25,9 @@ export * from './accounts/pollingInsuranceFundStakeAccountSubscriber'; export * from './accounts/pollingHighLeverageModeConfigAccountSubscriber'; export * from './accounts/basicUserAccountSubscriber'; export * from './accounts/oneShotUserAccountSubscriber'; +export * from './constituentMap/constituentMap'; +export * from './constituentMap/pollingConstituentAccountSubscriber'; +export * from './constituentMap/webSocketConstituentAccountSubscriber'; export * from './accounts/types'; export * from './addresses/pda'; export * from './adminClient'; diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 300f2a75d0..f02a4ef0f3 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -112,3 +112,14 @@ export function getSignedMsgUserOrdersFilter(): MemcmpFilter { }, }; } + +export function getConstituentFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('Constituent') + ), + }, + }; +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 1adc11c173..769835527c 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -726,6 +726,54 @@ export type TransferProtocolIfSharesToRevenuePoolRecord = { transferAmount: BN; }; +export type LPSwapRecord = { + ts: BN; + slot: BN; + authority: PublicKey; + outAmount: BN; + inAmount: BN; + outFee: BN; + inFee: BN; + outSpotMarketIndex: number; + inSpotMarketIndex: number; + outConstituentIndex: number; + inConstituentIndex: number; + outOraclePrice: BN; + inOraclePrice: BN; + outMint: PublicKey; + inMint: PublicKey; + lastAum: BN; + lastAumSlot: BN; + inMarketCurrentWeight: BN; + outMarketCurrentWeight: BN; + inMarketTargetWeight: BN; + outMarketTargetWeight: BN; + inSwapId: BN; + outSwapId: BN; +}; + +export type LPMintRedeemRecord = { + ts: BN; + slot: BN; + authority: PublicKey; + isMinting: boolean; + amount: BN; + fee: BN; + spotMarketIndex: number; + constituentIndex: number; + oraclePrice: BN; + mint: PublicKey; + lpMint: PublicKey; + lpAmount: BN; + lpFee: BN; + lpPrice: BN; + mintRedeemId: BN; + lastAum: BN; + lastAumSlot: BN; + inMarketCurrentWeight: BN; + inMarketTargetWeight: BN; +}; + export type StateAccount = { admin: PublicKey; exchangeStatus: number; @@ -1554,3 +1602,117 @@ export type SignedMsgUserOrdersAccount = { authorityPubkey: PublicKey; signedMsgOrderData: SignedMsgOrderId[]; }; + +export type AddAmmConstituentMappingDatum = { + constituentIndex: number; + perpMarketIndex: number; + weight: BN; +}; + +export type AmmConstituentDatum = AddAmmConstituentMappingDatum & { + lastSlot: BN; +}; + +export type AmmConstituentMapping = { + bump: number; + weights: AmmConstituentDatum[]; +}; + +export type TargetDatum = { + costToTradeBps: number; + beta: number; + targetBase: BN; + lastSlot: BN; +}; + +export type ConstituentTargetBase = { + bump: number; + targets: TargetDatum[]; +}; + +export type LPPoolAccount = { + name: number[]; + pubkey: PublicKey; + mint: PublicKey; + maxAum: BN; + lastAum: BN; + lastAumSlot: BN; + lastAumTs: BN; + bump: number; + oldestOracleSlot: BN; + lastRevenueRebalanceTs: BN; + totalFeesReceived: BN; + totalFeesPaid: BN; + constituents: number; +}; + +export type BLPosition = { + scaledBalance: BN; + cumulativeDeposits: BN; + marketIndex: number; + balanceType: SpotBalanceType; +}; + +export type InitializeConstituentParams = { + spotMarketIndex: number; + decimals: number; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + maxBorrowTokenAmount: BN; + oracleStalenessThreshold: BN; + costToTrade: number; + derivativeWeight: BN; + constituentDerivativeIndex?: number; + constituentDerivativeDepegThreshold?: BN; + constituentCorrelations: BN[]; + volatility: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; +}; + +export type ConstituentAccount = { + pubkey: PublicKey; + spotMarketIndex: number; + constituentIndex: number; + decimals: number; + bump: number; + constituentDerivativeIndex: number; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + totalSwapFees: BN; + tokenBalance: BN; + spotBalance: BLPosition; + lastOraclePrice: BN; + lastOracleSlot: BN; + mint: PublicKey; + oracleStalenessThreshold: BN; + lpPool: PublicKey; + tokenVault: PublicKey; + nextSwapId: BN; + derivativeWeight: BN; + flashLoanInitialTokenAmount: BN; +}; + +export type CacheInfo = { + slot: BN; + position: BN; + maxConfidenceIntervalMultiplier: BN; + lastOraclePriceTwap: BN; + oracle: PublicKey; + oracleSource: number; + oraclePrice: BN; + oracleDelay: BN; + oracleConfidence: BN; + lastFeePoolTokenAmount: BN; + lastNetPnlPoolTokenAmount: BN; + lastSettleAmount: BN; + lastSettleTs: BN; + quoteOwedFromLpPool: BN; +}; + +export type AmmCache = { + cache: CacheInfo[]; +}; diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 2775260207..f3fb157085 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,10 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(referencePriceOffset.ts) +test_files=( + lpPool.ts + lpPoolSwap.ts +) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/fixtures/token_2022.so b/tests/fixtures/token_2022.so new file mode 100755 index 0000000000..23c12ecb2f Binary files /dev/null and b/tests/fixtures/token_2022.so differ diff --git a/tests/lpPool.ts b/tests/lpPool.ts new file mode 100644 index 0000000000..2c56b4441d --- /dev/null +++ b/tests/lpPool.ts @@ -0,0 +1,1464 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; +import { getAssociatedTokenAddress, getMint } from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + encodeName, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBase, + AmmConstituentMapping, + LPPoolAccount, + getConstituentVaultPublicKey, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + getPythLazerOraclePublicKey, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + getTokenAmount, + TWO, +} from '../sdk/src'; + +import { + initializeQuoteSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overWriteMintAccount, + overWritePerpMarket, + overWriteSpotMarket, + setFeedPriceNoProgram, +} 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 { PYTH_LAZER_HEX_STRING_SOL, PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + let spotMarketOracle2: PublicKey; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + let solUsdLazer: PublicKey; + + const lpPoolName = 'test pool 1'; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey( + program.programId, + encodeName(lpPoolName) + ); + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'token_2022', + programId: new PublicKey( + 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb' + ), + }, + ], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + const periodicity = new BN(0); + + solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); + await adminClient.initializePythLazerOracle(6); + + await adminClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + await adminClient.initializePerpMarket( + 2, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(2, 1); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + await adminClient.initializeLpPool( + lpPoolName, + ZERO, + ZERO, + new BN(3600), + new BN(1_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + + // Give the vamm some inventory + await adminClient.openPosition(PositionDirection.LONG, BASE_PRECISION, 0); + await adminClient.openPosition(PositionDirection.SHORT, BASE_PRECISION, 1); + assert( + adminClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 2 + ); + }); + + after(async () => { + await adminClient.unsubscribe(); + }); + + it('can create a new LP Pool', async () => { + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal( + adminClient.getSignerPublicKey().toBase58() + ); + }); + + it('can add constituents to LP Pool', async () => { + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 1); + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentTokenVault = + await bankrunContextWrapper.connection.getAccountInfo( + constituentVaultPublicKey + ); + expect(constituentTokenVault).to.not.be.null; + + // Add second constituent representing SOL + await adminClient.initializeConstituent(lpPool.name, { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO], + }); + }); + + it('can add amm mapping datum', async () => { + // Firt constituent is USDC, so add no mapping. We will add a second mapping though + // for the second constituent which is SOL + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 1, + constituentIndex: 1, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 1); + }); + + it('can update and remove amm constituent mapping entries', async () => { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + let ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 2); + + // Update + await adminClient.updateAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION.muln(2), + }, + ]); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert( + ammMapping.weights + .find((x) => x.perpMarketIndex == 2) + .weight.eq(PERCENTAGE_PRECISION.muln(2)) + ); + + // Remove + await adminClient.removeAmmConstituentMappingData( + encodeName(lpPoolName), + 2, + 0 + ); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.find((x) => x.perpMarketIndex == 2) == undefined); + assert(ammMapping.weights.length === 1); + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == 3); + assert(ammCache.cache[0].oracle.equals(solUsd)); + assert(ammCache.cache[0].oraclePrice.eq(new BN(200000000))); + }); + + it('can update constituent properties and correlations', async () => { + const constituentPublicKey = getConstituentPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + constituentPublicKey + )) as ConstituentAccount; + + await adminClient.updateConstituentParams( + encodeName(lpPoolName), + constituentPublicKey, + { + costToTradeBps: 10, + } + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const targets = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBase + )) as ConstituentTargetBase; + expect(targets).to.not.be.null; + assert(targets.targets[constituent.constituentIndex].costToTradeBps == 10); + + await adminClient.updateConstituentCorrelationData( + encodeName(lpPoolName), + 0, + 1, + PERCENTAGE_PRECISION.muln(87).divn(100) + ); + + await adminClient.updateConstituentCorrelationData( + encodeName(lpPoolName), + 0, + 1, + PERCENTAGE_PRECISION + ); + }); + + it('fails adding datum with bad params', async () => { + // Bad perp market index + try { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 3, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + expect(e.message).to.contain('0x18ae'); + } + + // Bad constituent index + try { + await adminClient.addAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 0, + constituentIndex: 5, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + expect(e.message).to.contain('0x18ae'); + } + }); + + it('fails to add liquidity if aum not updated atomically', async () => { + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.lpPoolAddLiquidity({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + }); + expect.fail('should have failed'); + } catch (e) { + assert(e.message.includes('0x18b7')); + } + }); + + it('can update pool aum', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.constituents == 2); + + const createAtaIx = + adminClient.createAssociatedTokenAccountIdempotentInstruction( + await getAssociatedTokenAddress( + lpPool.mint, + adminClient.wallet.publicKey, + true + ), + adminClient.wallet.publicKey, + adminClient.wallet.publicKey, + lpPool.mint + ); + + await adminClient.sendTransaction(new Transaction().add(createAtaIx), []); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + }) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.lastAum.eq(new BN(1000).mul(QUOTE_PRECISION))); + + // Should fail if we dont pass in the second constituent + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + try { + await adminClient.updateLpPoolAum(lpPool, [0]); + expect.fail('should have failed'); + } catch (e) { + assert(e.message.includes('0x18b3')); + } + }); + + it('can update constituent target weights', async () => { + await adminClient.postPythLazerOracleUpdate([6], PYTH_LAZER_HEX_STRING_SOL); + await adminClient.updatePerpMarketOracle( + 0, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 1, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 2, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updateAmmCache([0, 1, 2]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ] + ) + ); + await adminClient.sendTransaction(tx); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + assert( + constituentTargetBase.targets.filter((x) => x.targetBase.eq(ZERO)) + .length !== constituentTargetBase.targets.length + ); + }); + + it('can add constituent to LP Pool thats a derivative and behave correctly', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.name, { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + oracleStalenessThreshold: new BN(400), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION.muln(87).divn(100)], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ] + ) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[2].targetBase.toNumber(), + 10 + ); + + // Move the oracle price to be double, so it should have half of the target base + const derivativeBalanceBefore = constituentTargetBase.targets[2].targetBase; + const derivative = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + await setFeedPriceNoProgram(bankrunContextWrapper, 400, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ] + ) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + const derivativeBalanceAfter = constituentTargetBase.targets[2].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + expect(derivativeBalanceAfter.toNumber()).to.be.approximately( + derivativeBalanceBefore.toNumber() / 2, + 20 + ); + + // Move the oracle price to be half, so its target base should go to zero + const parentBalanceBefore = constituentTargetBase.targets[1].targetBase; + await setFeedPriceNoProgram(bankrunContextWrapper, 100, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx3 = new Transaction(); + tx3 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ] + ) + ); + await adminClient.sendTransaction(tx3); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + const parentBalanceAfter = constituentTargetBase.targets[1].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect(parentBalanceAfter.toNumber()).to.be.approximately( + parentBalanceBefore.toNumber() * 2, + 10 + ); + await setFeedPriceNoProgram(bankrunContextWrapper, 200, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + }); + + it('can settle pnl from perp markets into the usdc account', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.depositIntoPerpMarketFeePool( + 0, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + await adminClient.depositIntoPerpMarketFeePool( + 1, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterDeposit = lpPool.lastAum; + + // Make sure the amount recorded goes into the cache and that the quote amount owed is adjusted + // for new influx in fees + const ammCacheBeforeAdjust = ammCache; + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert(ammCache.cache[1].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool.sub( + new BN(100).mul(QUOTE_PRECISION) + ) + ) + ); + assert( + ammCache.cache[1].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[1].quoteOwedFromLpPool.sub( + new BN(100).mul(QUOTE_PRECISION) + ) + ) + ); + + const usdcBefore = constituent.tokenBalance; + // Update Amm Cache to update the aum + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterUpdateCacheBeforeSettle = lpPool.lastAum; + assert( + lpAumAfterUpdateCacheBeforeSettle.eq( + lpAumAfterDeposit.add(new BN(200).mul(QUOTE_PRECISION)) + ) + ); + + // Calculate the expected transfer amount which is the increase in fee pool - amount owed, + // but we have to consider the fee pool limitations + const pnlPoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + const pnlPoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + // Expected transfers per pool are capital constrained by the actual balances + const expectedTransfer0 = BN.min( + ammCache.cache[0].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance0.add(feePoolBalance0) + ); + const expectedTransfer1 = BN.min( + ammCache.cache[1].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance1.add(feePoolBalance1) + ); + const expectedTransferAmount = expectedTransfer0.add(expectedTransfer1); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterSettle = lpPool.lastAum; + assert(lpAumAfterSettle.eq(lpAumAfterUpdateCacheBeforeSettle)); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const usdcAfter = constituent.tokenBalance; + const feePoolBalanceAfter = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + console.log('usdcBefore', usdcBefore.toString()); + console.log('usdcAfter', usdcAfter.toString()); + + // Verify the expected usdc transfer amount + assert(usdcAfter.sub(usdcBefore).eq(expectedTransferAmount)); + console.log('feePoolBalanceBefore', feePoolBalance0.toString()); + console.log('feePoolBalanceAfter', feePoolBalanceAfter.toString()); + // Fee pool can cover it all in first perp market + assert(feePoolBalance0.sub(feePoolBalanceAfter).eq(expectedTransfer0)); + + // Constituent sync worked successfully + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + assert( + new BN(constituentVault.amount.toString()).eq(constituent.tokenBalance) + ); + }); + + it('will settle gracefully when trying to settle pnl from constituents to perp markets if not enough usdc in the constituent vault', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + /// First remove some liquidity so DLP doesnt have enought to transfer + const lpTokenBalance = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(lpTokenBalance.amount.toString()), + minAmountOut: new BN(1000).mul(QUOTE_PRECISION), + lpPool: lpPool, + }) + ); + await adminClient.sendTransaction(tx); + + let constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + const expectedTransferAmount = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const constituentUSDCBalanceBefore = constituentVault.amount; + + // Temporarily overwrite perp market to have taken a loss on the fee pool + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const spotMarket = adminClient.getSpotMarketAccount(0); + const perpMarket = adminClient.getPerpMarketAccount(0); + spotMarket.depositBalance = spotMarket.depositBalance.sub( + perpMarket.amm.feePool.scaledBalance.add( + spotMarket.cumulativeDepositInterest.muln(10 ** 3) + ) + ); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + perpMarket.amm.feePool.scaledBalance = ZERO; + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + /// Now finally try and settle Perp to LP Pool + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + await adminClient.sendTransaction(settleTx); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + constituentVault = await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + // Should have written fee pool amount owed to the amm cache and new constituent usdc balane should be 0 + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + // No more usdc left in the constituent vault + assert(constituent.tokenBalance.eq(ZERO)); + assert(new BN(constituentVault.amount.toString()).eq(ZERO)); + + // Should have recorded the amount left over to the amm cache and increased the amount in the fee pool + assert( + ammCache.cache[0].lastFeePoolTokenAmount.eq( + new BN(constituentUSDCBalanceBefore.toString()) + ) + ); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + expectedTransferAmount.sub( + new BN(constituentUSDCBalanceBefore.toString()) + ) + ) + ); + assert( + adminClient + .getPerpMarketAccount(0) + .amm.feePool.scaledBalance.eq( + new BN(constituentUSDCBalanceBefore.toString()).mul( + SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION) + ) + ) + ); + + // Update the LP pool AUM + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.lastAum.eq(ZERO)); + }); + + it('perp market will not transfer with the constituent vault if it is owed from dlp', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market half of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .div(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(ammCache.cache[0].quoteOwedFromLpPool.eq(owedAmount.divn(2))); + assert(constituent.tokenBalance.eq(ZERO)); + assert(lpPool.lastAum.eq(ZERO)); + + // Deposit here to DLP to make sure aum calc work with perp market debt + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + }) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + let aum = new BN(0); + for (let i = 0; i <= 2; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + aum = aum.add( + constituent.tokenBalance + .mul(constituent.lastOraclePrice) + .div(QUOTE_PRECISION) + ); + } + + // Overwrite the amm cache with amount owed + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + for (let i = 0; i <= ammCache.cache.length - 1; i++) { + aum = aum.sub(ammCache.cache[i].quoteOwedFromLpPool); + } + assert(lpPool.lastAum.eq(aum)); + }); + + it('perp market will transfer with the constituent vault if it should send more than its owed', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const aumBefore = lpPool.lastAum; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + const balanceBefore = constituent.tokenBalance; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market half of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .mul(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx( + encodeName(lpPoolName), + [0, 1, 2] + ) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(ammCache.cache[0].quoteOwedFromLpPool.eq(ZERO)); + assert(constituent.tokenBalance.eq(balanceBefore.add(owedAmount))); + assert(lpPool.lastAum.eq(aumBefore.add(owedAmount.muln(2)))); + }); + + it('can work with multiple derivatives on the same parent', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.name, { + spotMarketIndex: 3, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ + ZERO, + PERCENTAGE_PRECISION.muln(87).divn(100), + PERCENTAGE_PRECISION, + ], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateConstituentParams( + lpPool.name, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + } + ); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ] + ) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[2].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[3].targetBase.toNumber(), + 10 + ); + expect( + constituentTargetBase.targets[3].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[1].targetBase.toNumber() / 2, + 10 + ); + + // Set the derivative weights to 0 + await adminClient.updateConstituentParams( + lpPool.name, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: ZERO, + } + ); + + await adminClient.updateConstituentParams( + lpPool.name, + getConstituentPublicKey(program.programId, lpPoolKey, 3), + { + derivativeWeight: ZERO, + } + ); + + const parentTargetBaseBefore = constituentTargetBase.targets[1].targetBase; + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx( + encodeName(lpPoolName), + [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ] + ) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + const parentTargetBaseAfter = constituentTargetBase.targets[1].targetBase; + + expect(parentTargetBaseAfter.toNumber()).to.be.approximately( + parentTargetBaseBefore.toNumber() * 2, + 10 + ); + }); + + it('cant withdraw more than constituent limit', async () => { + await adminClient.updateConstituentParams( + encodeName(lpPoolName), + getConstituentPublicKey(program.programId, lpPoolKey, 0), + { + maxBorrowTokenAmount: new BN(10).muln(10 ** 6), + } + ); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + const balanceBefore = constituent.tokenBalance; + const spotBalanceBefore = constituent.spotBalance; + + await adminClient.withdrawFromProgramVault( + encodeName(lpPoolName), + 0, + new BN(100).mul(QUOTE_PRECISION) + ); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + assert( + constituent.tokenBalance + .sub(balanceBefore) + .eq(new BN(10).mul(QUOTE_PRECISION)) + ); + expect( + constituent.spotBalance.scaledBalance + .sub(spotBalanceBefore.scaledBalance) + .toNumber() + ).to.be.approximately(10 * 10 ** 9, 1); + }); +}); diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts new file mode 100644 index 0000000000..9e50652670 --- /dev/null +++ b/tests/lpPoolSwap.ts @@ -0,0 +1,902 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; +import { Program } from '@coral-xyz/anchor'; +import { + Account, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + encodeName, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBase, + OracleSource, + SPOT_MARKET_RATE_PRECISION, + SPOT_MARKET_WEIGHT_PRECISION, + LPPoolAccount, + convertToNumber, + getConstituentVaultPublicKey, + getConstituentPublicKey, + ConstituentAccount, + ZERO, + getSerumSignerPublicKey, + BN_MAX, + isVariant, +} from '../sdk/src'; +import { + initializeQuoteSpotMarket, + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + overWriteTokenAccountBalance, + 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', () => { + const program = anchor.workspace.Drift as Program; + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + + let serumMarketPublicKey: PublicKey; + + let serumDriftClient: TestClient; + let serumWSOL: PublicKey; + let serumUSDC: PublicKey; + let serumKeypair: Keypair; + + let adminSolAta: PublicKey; + + 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 + ); + const ammInitialBaseAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const lpPoolName = 'test pool 1'; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey( + program.programId, + encodeName(lpPoolName) + ); + + let userUSDCAccount: Keypair; + let serumMarket: Market; + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200.1); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 50 * LAMPORTS_PER_SOL); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: [0, 1, 2], + oracleInfos: [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + new BN(10).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair.publicKey + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(10).mul(QUOTE_PRECISION), + userUSDCAccount.publicKey + ); + + const periodicity = new BN(0); + + await adminClient.initializePerpMarket( + 0, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + adminSolAta = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(20 * 10 ** 9) // 10 SOL + ); + + await adminClient.initializeLpPool( + lpPoolName, + new BN(100), // 1 bps + ZERO, // 1 bps + new BN(3600), + new BN(100_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() // dlp mint + ); + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION, + volatility: ZERO, + constituentCorrelations: [], + }); + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln(4).divn(100), + constituentCorrelations: [ZERO], + }); + + await initializeSolSpotMarket(adminClient, spotMarketOracle); + await adminClient.updateSpotMarketStepSizeAndTickSize( + 2, + new BN(100000000), + new BN(100) + ); + await adminClient.updateSpotAuctionDuration(0); + + await adminClient.deposit( + new BN(5 * 10 ** 9), // 10 SOL + 2, // market index + adminSolAta // user token account + ); + + await adminClient.depositIntoSpotMarketVault( + 2, + new BN(4 * 10 ** 9), // 4 SOL + adminSolAta + ); + + [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 () => { + await adminClient.unsubscribe(); + await serumDriftClient.unsubscribe(); + }); + + it('LP Pool init properly', async () => { + let lpPool: LPPoolAccount; + try { + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool).to.not.be.null; + } catch (e) { + expect.fail('LP Pool should have been created'); + } + + try { + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBase; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + } catch (e) { + expect.fail('Amm constituent map should have been created'); + } + }); + + it('lp pool swap', async () => { + let spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price1 = convertToNumber(spotOracle.price); + + await setFeedPriceNoProgram(bankrunContextWrapper, 224.3, spotMarketOracle); + + await adminClient.fetchAccounts(); + + spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price2 = convertToNumber(spotOracle.price); + assert(price2 > price1); + + const const0TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const const1TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 1 + ); + + const const0Key = getConstituentPublicKey(program.programId, lpPoolKey, 0); + const const1Key = getConstituentPublicKey(program.programId, lpPoolKey, 1); + + const c0TokenBalance = new BN(224_300_000_000); + const c1TokenBalance = new BN(1_000_000_000); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const0TokenAccount, + BigInt(c0TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const0Key, + [['tokenBalance', c0TokenBalance]] + ); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const1TokenAccount, + BigInt(c1TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const1Key, + [['tokenBalance', c1TokenBalance]] + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + const0Key + )) as ConstituentAccount; + expect(c0.tokenBalance.toString()).to.equal(c0TokenBalance.toString()); + + const c1 = (await adminClient.program.account.constituent.fetch( + const1Key + )) as ConstituentAccount; + expect(c1.tokenBalance.toString()).to.equal(c1TokenBalance.toString()); + + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const prec = new BN(10).pow(new BN(tokenDecimals)); + console.log(`const0 balance: ${convertToNumber(c0.tokenBalance, prec)}`); + console.log(`const1 balance: ${convertToNumber(c1.tokenBalance, prec)}`); + + const lpPool1 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool1.lastAumSlot.toNumber()).to.be.equal(0); + + await adminClient.updateLpPoolAum(lpPool1, [1, 0]); + + const lpPool2 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect(lpPool2.lastAumSlot.toNumber()).to.be.greaterThan(0); + expect(lpPool2.lastAum.gt(lpPool1.lastAum)).to.be.true; + console.log(`AUM: ${convertToNumber(lpPool2.lastAum, QUOTE_PRECISION)}`); + + const constituentTargetWeightsPublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + // swap c0 for c1 + + const adminAuth = adminClient.wallet.publicKey; + + // mint some tokens for user + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(224_300_000_000), + adminAuth + ); + const c1UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + spotTokenMint.publicKey, + new BN(1_000_000_000), + adminAuth + ); + + const inTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + + // in = 0, out = 1 + const swapTx = new Transaction(); + swapTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool2, [0, 1])); + swapTx.add( + await adminClient.getLpPoolSwapIx( + 0, + 1, + new BN(224_300_000), + new BN(0), + lpPoolKey, + constituentTargetWeightsPublicKey, + const0TokenAccount, + const1TokenAccount, + c0UserTokenAccount, + c1UserTokenAccount, + const0Key, + const1Key, + usdcMint.publicKey, + spotTokenMint.publicKey + ) + ); + await adminClient.sendTransaction(swapTx); + + const inTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + const diffInToken = + inTokenBalanceAfter.amount - inTokenBalanceBefore.amount; + const diffOutToken = + outTokenBalanceAfter.amount - outTokenBalanceBefore.amount; + + expect(Number(diffInToken)).to.be.equal(-224_300_000); + expect(Number(diffOutToken)).to.be.approximately(1001298, 1); + + console.log( + `in Token: ${inTokenBalanceBefore.amount} -> ${ + inTokenBalanceAfter.amount + } (${Number(diffInToken) / 1e6})` + ); + console.log( + `out Token: ${outTokenBalanceBefore.amount} -> ${ + outTokenBalanceAfter.amount + } (${Number(diffOutToken) / 1e6})` + ); + }); + + it('lp pool add and remove liquidity: usdc', async () => { + // add c0 liquidity + const adminAuth = adminClient.wallet.publicKey; + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(1_000_000_000_000), + adminAuth + ); + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumBefore = lpPool.lastAum; + + const userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminAuth + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + const c1 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const userC0TokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tokensAdded = new BN(1_000_000_000_000); + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + }) + ); + await adminClient.sendTransaction(tx); + + const userC0TokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumAfter = lpPool.lastAum; + const lpPoolAumDiff = lpPoolAumAfter.sub(lpPoolAumBefore); + expect(lpPoolAumDiff.toString()).to.be.equal(tokensAdded.toString()); + + const userC0TokenBalanceDiff = + Number(userC0TokenBalanceAfter.amount) - + Number(userC0TokenBalanceBefore.amount); + expect(Number(userC0TokenBalanceDiff)).to.be.equal( + -1 * tokensAdded.toNumber() + ); + + const userLpTokenBalanceDiff = + Number(userLpTokenBalanceAfter.amount) - + Number(userLpTokenBalanceBefore.amount); + expect(userLpTokenBalanceDiff).to.be.equal( + (((tokensAdded.toNumber() * 9997) / 10000) * 9999) / 10000 + ); // max weight deviation: expect min swap% fee on constituent, + 0.01% lp mint fee + + // remove liquidity + const removeTx = new Transaction(); + removeTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + removeTx.add( + await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(userLpTokenBalanceAfter.amount.toString()), + minAmountOut: new BN(1), + lpPool: lpPool, + }) + ); + await adminClient.sendTransaction(removeTx); + + const userC0TokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const userC0TokenBalanceAfterBurnDiff = + Number(userC0TokenBalanceAfterBurn.amount) - + Number(userC0TokenBalanceAfter.amount); + + expect(userC0TokenBalanceAfterBurnDiff).to.be.greaterThan(0); + expect(Number(userLpTokenBalanceAfterBurn.amount)).to.be.equal(0); + + const totalC0TokensLost = new BN( + userC0TokenBalanceAfterBurn.amount.toString() + ).sub(tokensAdded); + const totalC0TokensLostPercent = + Number(totalC0TokensLost) / Number(tokensAdded); + expect(totalC0TokensLostPercent).to.be.approximately(-0.0006, 0.0001); // lost about 7bps 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 sol for usdc', async () => { + // Initialize new constituent for market 2 + await adminClient.initializeConstituent(encodeName(lpPoolName), { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION], + }); + + 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); + }); + + it('deposit and withdraw atomically before swapping', async () => { + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + await adminClient.depositWithdrawToProgramVault( + encodeName(lpPoolName), + 0, + 2, + new BN(400).mul(QUOTE_PRECISION), // 100 USDC + new BN(2 * 10 ** 9) // 100 USDC + ); + + 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(-400000000); + expect(solDiff).to.be.equal(2000000000); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + assert(constituent.spotBalance.scaledBalance.eq(new BN(2000000001))); + assert(isVariant(constituent.spotBalance.balanceType, 'borrow')); + }); +}); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index f6a95c468f..a2b29f5021 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -43,6 +43,8 @@ import { PositionDirection, DriftClient, OrderType, + ConstituentAccount, + SpotMarketAccount, } from '../sdk'; import { TestClient, @@ -1205,6 +1207,23 @@ export async function overWritePerpMarket( }); } +export async function overWriteSpotMarket( + driftClient: TestClient, + bankrunContextWrapper: BankrunContextWrapper, + spotMarketKey: PublicKey, + spotMarket: SpotMarketAccount +) { + bankrunContextWrapper.context.setAccount(spotMarketKey, { + executable: false, + owner: driftClient.program.programId, + lamports: LAMPORTS_PER_SOL, + data: await driftClient.program.account.spotMarket.coder.accounts.encode( + 'SpotMarket', + spotMarket + ), + }); +} + export async function getPerpMarketDecoded( driftClient: TestClient, bankrunContextWrapper: BankrunContextWrapper, @@ -1358,3 +1377,29 @@ export async function placeAndFillVammTrade({ console.error(e); } } + +export async function overwriteConstituentAccount( + bankrunContextWrapper: BankrunContextWrapper, + program: Program, + constituentPublicKey: PublicKey, + overwriteFields: Array<[key: keyof ConstituentAccount, value: any]> +) { + const acc = await program.account.constituent.fetch(constituentPublicKey); + if (!acc) { + throw new Error( + `Constituent account ${constituentPublicKey.toBase58()} not found` + ); + } + for (const [key, value] of overwriteFields) { + acc[key] = value; + } + bankrunContextWrapper.context.setAccount(constituentPublicKey, { + executable: false, + owner: program.programId, + lamports: LAMPORTS_PER_SOL, + data: await program.account.constituent.coder.accounts.encode( + 'Constituent', + acc + ), + }); +}