diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index c74a80ea5d..293fe3997f 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -676,6 +676,8 @@ pub enum ErrorCode { MintRedeemLpPoolDisabled, #[msg("Settlement amount exceeded")] LpPoolSettleInvariantBreached, + #[msg("Invalid constituent operation")] + InvalidConstituentOperation, } #[macro_export] diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index e4c7232339..df2415d936 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -110,3 +110,8 @@ pub mod amm_spread_adjust_wallet { #[cfg(feature = "anchor-test")] declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); } + +pub mod lp_pool_swap_wallet { + use solana_program::declare_id; + declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); +} diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 1a813cdfd0..02639bb628 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,4 +1,3 @@ -use crate::math::amm::calculate_net_user_pnl; use crate::{msg, FeatureBitFlags}; use anchor_lang::prelude::*; use anchor_spl::token_2022::Token2022; @@ -62,8 +61,8 @@ 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, SpotBalance, SpotBalanceType, SpotFulfillmentConfigStatus, - SpotMarket, TokenProgramFlag, + AssetTier, InsuranceFund, SpotBalanceType, SpotFulfillmentConfigStatus, SpotMarket, + TokenProgramFlag, }; use crate::state::spot_market_map::get_writable_spot_market_set; use crate::state::state::{ @@ -972,10 +971,10 @@ pub fn handle_initialize_perp_market( protected_maker_dynamic_divisor: 0, lp_fee_transfer_scalar: 1, lp_status: 0, - padding1: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, - lp_exchange_fee_excluscion_scalar: 1, - padding: [0; 23], + padding: [0; 24], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, @@ -4244,6 +4243,16 @@ pub fn handle_update_perp_market_lp_pool_status( Ok(()) } +pub fn handle_update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + perp_market.lp_paused_operations = lp_paused_operations; + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index cc2b2fa210..4d304e35d8 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -53,6 +53,7 @@ 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::PerpLpOperation; use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::{ContractType, MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ @@ -3149,7 +3150,12 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( 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 { + if perp_market.lp_status == 0 + || PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::SettleQuoteOwed, + ) + { continue; } @@ -3344,7 +3350,14 @@ pub fn handle_update_amm_cache<'c: 'info, 'info>( &state.oracle_guard_rails, )?; - amm_cache.update_amount_owed_from_lp_pool(&perp_market, "e_market)?; + if perp_market.lp_status != 0 + && !PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::TrackAmmRevenue, + ) + { + amm_cache.update_amount_owed_from_lp_pool(&perp_market, "e_market)?; + } } Ok(()) diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs index d887427aad..ecf200975c 100644 --- a/programs/drift/src/instructions/lp_admin.rs +++ b/programs/drift/src/instructions/lp_admin.rs @@ -1,13 +1,13 @@ use crate::controller; use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; use crate::error::ErrorCode; -use crate::ids::admin_hot_wallet; +use crate::ids::{admin_hot_wallet, lp_pool_swap_wallet}; use crate::instructions::optional_accounts::get_token_mint; use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; use crate::math::safe_math::SafeMath; use crate::state::lp_pool::{ AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, - ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, + ConstituentStatus, ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, CONSTITUENT_CORRELATIONS_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_BASE_PDA_SEED, CONSTITUENT_VAULT_PDA_SEED, }; @@ -215,6 +215,34 @@ pub fn handle_initialize_constituent<'info>( Ok(()) } +pub fn handle_update_constituent_status<'info>( + ctx: Context, + new_status: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent status: {:?} -> {:?}", + constituent.status, + new_status + ); + constituent.status = new_status; + Ok(()) +} + +pub fn handle_update_constituent_paused_operations<'info>( + ctx: Context, + paused_operations: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent paused operations: {:?} -> {:?}", + constituent.paused_operations, + paused_operations + ); + constituent.paused_operations = paused_operations; + Ok(()) +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct ConstituentParams { pub max_weight_deviation: Option, @@ -546,6 +574,21 @@ pub fn handle_begin_lp_swap<'c: 'info, 'info>( out_market_index: u16, amount_in: u64, ) -> Result<()> { + // Check admin + let admin = &ctx.accounts.admin; + #[cfg(feature = "anchor-test")] + validate!( + admin.key() == admin_hot_wallet::id() || admin.key() == state.admin, + ErrorCode::Unauthorized, + "Wrong signer for lp taker swap" + )?; + #[cfg(not(feature = "anchor-test"))] + validate!( + admin.key() == lp_pool_swap_wallet::id(), + ErrorCode::DefaultError, + "Wrong signer for lp taker swap" + )?; + let ixs = ctx.accounts.instructions.as_ref(); let current_index = instructions::load_current_index_checked(ixs)? as usize; @@ -1012,6 +1055,30 @@ pub struct UpdateConstituentParams<'info> { pub constituent: AccountLoader<'info, Constituent>, } +#[derive(Accounts)] +pub struct UpdateConstituentStatus<'info> { + #[account( + mut, + constraint = admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentPausedOperations<'info> { + #[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)] @@ -1134,10 +1201,7 @@ pub struct UpdateConstituentCorrelation<'info> { )] pub struct LPTakerSwap<'info> { pub state: Box>, - #[account( - mut, - constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin - )] + #[account(mut)] pub admin: Signer<'info>, /// Signer token accounts diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index 2e5d9b8aa3..6e164b458a 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -3,6 +3,7 @@ use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::math::constants::{PERCENTAGE_PRECISION, PRICE_PRECISION_I64}; use crate::math::oracle::OracleValidity; +use crate::state::paused_operations::ConstituentLpOperation; use crate::validation::whitelist::validate_whitelist_token; use crate::{ controller::{ @@ -285,6 +286,9 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = ctx.accounts.constituent_target_base.load_zc()?; @@ -320,6 +324,20 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>( let in_spot_market = spot_market_map.get_ref(&in_market_index)?; let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + let in_oracle_id = in_spot_market.oracle_id(); let out_oracle_id = out_spot_market.oracle_id(); @@ -593,6 +611,9 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( "Mint/redeem LP pool is disabled" )?; + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Deposit)?; + let slot = Clock::get()?.slot; let now = Clock::get()?.unix_timestamp; let lp_pool_key = ctx.accounts.lp_pool.key(); @@ -611,8 +632,6 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); - let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; - let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = ctx.accounts.constituent_target_base.load_zc()?; @@ -646,6 +665,13 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + let in_oracle_id = in_spot_market.oracle_id(); let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( @@ -936,6 +962,9 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( let lp_price_before = lp_pool.get_price(ctx.accounts.lp_mint.supply)?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Withdraw)?; + // Verify previous settle let amm_cache: AccountZeroCopy<'_, CacheInfo, _> = ctx.accounts.amm_cache.load_zc()?; for (i, _) in amm_cache.iter().enumerate() { @@ -959,8 +988,6 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( return Err(ErrorCode::LpPoolAumDelayed.into()); } - let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; - let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = ctx.accounts.constituent_target_base.load_zc()?; @@ -987,6 +1014,13 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + let out_oracle_id = out_spot_market.oracle_id(); let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 1367737fda..d6867d2636 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1069,6 +1069,13 @@ pub mod drift { handle_update_perp_market_expiry(ctx, expiry_ts) } + pub fn update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_paused_operations(ctx, lp_paused_operations) + } + pub fn update_perp_market_lp_pool_status( ctx: Context, lp_status: u8, @@ -1922,6 +1929,20 @@ pub mod drift { ) } + pub fn update_constituent_status<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentStatus<'info>>, + new_status: u8, + ) -> Result<()> { + handle_update_constituent_status(ctx, new_status) + } + + pub fn update_constituent_paused_operations<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentPausedOperations<'info>>, + paused_operations: u8, + ) -> Result<()> { + handle_update_constituent_paused_operations(ctx, paused_operations) + } + pub fn update_constituent_params( ctx: Context, constituent_params: ConstituentParams, diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index c774ff9816..6b9e320af8 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -7,12 +7,15 @@ use crate::math::constants::{ PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, }; use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; use crate::state::amm_cache::{AmmCacheFixed, CacheInfo}; use crate::state::constituent_map::ConstituentMap; +use crate::state::paused_operations::ConstituentLpOperation; use crate::state::spot_market_map::SpotMarketMap; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use enumflags2::BitFlags; use super::oracle::OraclePriceData; use super::spot_market::SpotMarket; @@ -815,14 +818,77 @@ pub struct Constituent { pub gamma_inventory: u8, pub gamma_execution: u8, pub xi: u8, - pub _padding: [u8; 4], + + // Status + pub status: u8, + pub paused_operations: u8, + pub _padding: [u8; 2], } impl Size for Constituent { const SIZE: usize = 304; } +#[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentStatus { + /// fills only able to reduce liability + ReduceOnly = 0b00000001, + /// market has no remaining participants + Decommissioned = 0b00000010, +} + impl Constituent { + pub fn get_status(&self) -> DriftResult> { + BitFlags::::from_bits(usize::from(self.status)).safe_unwrap() + } + + pub fn is_decommissioned(&self) -> DriftResult { + Ok(self + .get_status()? + .contains(ConstituentStatus::Decommissioned)) + } + + pub fn is_reduce_only(&self) -> DriftResult { + Ok(self.get_status()?.contains(ConstituentStatus::ReduceOnly)) + } + + pub fn does_constituent_allow_operation( + &self, + operation: ConstituentLpOperation, + ) -> DriftResult<()> { + if self.is_decommissioned()? { + msg!( + "Constituent {:?}, spot market {}, is decommissioned", + self.pubkey, + self.spot_market_index + ); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } else if ConstituentLpOperation::is_operation_paused(self.paused_operations, operation) { + msg!( + "Constituent {:?}, spot market {}, is paused for operation {:?}", + self.pubkey, + self.spot_market_index, + operation + ); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } else { + Ok(()) + } + } + + pub fn is_operation_reducing( + &self, + spot_market: &SpotMarket, + is_increasing: bool, + ) -> DriftResult { + let current_balance_sign = self.get_full_token_amount(spot_market)?.signum(); + if current_balance_sign > 0 { + Ok(!is_increasing) + } else { + Ok(is_increasing) + } + } + /// 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_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { diff --git a/programs/drift/src/state/paused_operations.rs b/programs/drift/src/state/paused_operations.rs index 81a6ec2a3a..516470460a 100644 --- a/programs/drift/src/state/paused_operations.rs +++ b/programs/drift/src/state/paused_operations.rs @@ -97,3 +97,55 @@ impl InsuranceFundOperation { } } } + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum PerpLpOperation { + TrackAmmRevenue = 0b00000001, + SettleQuoteOwed = 0b00000010, +} + +const ALL_PERP_LP_OPERATIONS: [PerpLpOperation; 2] = [ + PerpLpOperation::TrackAmmRevenue, + PerpLpOperation::SettleQuoteOwed, +]; + +impl PerpLpOperation { + pub fn is_operation_paused(current: u8, operation: PerpLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_PERP_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +const ALL_CONSTITUENT_LP_OPERATIONS: [ConstituentLpOperation; 3] = [ + ConstituentLpOperation::Swap, + ConstituentLpOperation::Deposit, + ConstituentLpOperation::Withdraw, +]; + +impl ConstituentLpOperation { + pub fn is_operation_paused(current: u8, operation: ConstituentLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_CONSTITUENT_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index caf562c92d..c7fd453d69 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,17 +1,13 @@ -use crate::math::spot_balance::get_token_amount; use crate::state::pyth_lazer_oracle::PythLazerOracle; use crate::state::user::MarketType; -use crate::state::zero_copy::{AccountZeroCopy, AccountZeroCopyMut}; -use crate::{impl_zero_copy_loader, validate}; use anchor_lang::prelude::*; use crate::state::state::{State, ValidityGuardRails}; use std::cmp::max; -use std::convert::TryFrom; -use crate::controller::position::{PositionDelta, PositionDirection}; +use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; -use crate::math::amm::{self, calculate_net_user_pnl}; +use crate::math::amm::{self}; use crate::math::casting::Cast; #[cfg(test)] use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; @@ -36,7 +32,7 @@ use crate::state::oracle::{ get_prelaunch_price, get_sb_on_demand_price, get_switchboard_price, HistoricalOracleData, MMOraclePriceData, OraclePriceData, OracleSource, }; -use crate::state::spot_market::{AssetTier, SpotBalance, SpotBalanceType, SpotMarket}; +use crate::state::spot_market::{AssetTier, SpotBalance, SpotBalanceType}; use crate::state::traits::{MarketIndexOffset, Size}; use borsh::{BorshDeserialize, BorshSerialize}; @@ -46,7 +42,6 @@ use static_assertions::const_assert_eq; use super::oracle_map::OracleIdentifier; use super::protected_maker_mode_config::ProtectedMakerParams; -use super::zero_copy::HasLen; use crate::math::oracle::{oracle_validity, LogMode, OracleValidity}; #[cfg(test)] @@ -92,6 +87,23 @@ impl MarketStatus { } } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum LpStatus { + /// Not considered + #[default] + Uncollateralized, + /// all operations allowed + Active, + /// Decommissioning + Decommissioning, +} + +impl LpStatus { + pub fn is_collateralized(&self) -> bool { + !matches!(self, LpStatus::Uncollateralized) + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] pub enum ContractType { #[default] @@ -239,10 +251,10 @@ pub struct PerpMarket { pub protected_maker_dynamic_divisor: u8, pub lp_fee_transfer_scalar: u8, pub lp_status: u8, - pub padding1: u16, - pub last_fill_price: u64, + pub lp_paused_operations: u8, pub lp_exchange_fee_excluscion_scalar: u8, - pub padding: [u8; 23], + pub last_fill_price: u64, + pub padding: [u8; 24], } impl Default for PerpMarket { @@ -286,10 +298,10 @@ impl Default for PerpMarket { protected_maker_dynamic_divisor: 0, lp_fee_transfer_scalar: 0, lp_status: 0, - padding1: 0, - last_fill_price: 0, lp_exchange_fee_excluscion_scalar: 0, - padding: [0; 23], + lp_paused_operations: 0, + last_fill_price: 0, + padding: [0; 24], } } } diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index ffdce28f84..a3aa0f102f 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -22,6 +22,7 @@ import { TxParams, SwapReduceOnly, InitializeConstituentParams, + ConstituentStatus, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -5151,6 +5152,75 @@ export class AdminClient extends DriftClient { ]; } + public async updateConstituentStatus( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + const updateConstituentStatusIx = await this.getUpdateConstituentStatusIx( + constituent, + constituentStatus + ); + + const tx = await this.buildTransaction(updateConstituentStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentStatusIx( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + return await this.program.instruction.updateConstituentStatus( + constituentStatus, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentPausedOperations( + constituent: PublicKey, + pausedOperations: number + ): Promise { + const updateConstituentPausedOperationsIx = + await this.getUpdateConstituentPausedOperationsIx( + constituent, + pausedOperations + ); + + const tx = await this.buildTransaction(updateConstituentPausedOperationsIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentPausedOperationsIx( + constituent: PublicKey, + pausedOperations: number + ): Promise { + return await this.program.instruction.updateConstituentPausedOperations( + pausedOperations, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + public async updateConstituentParams( lpPoolName: number[], constituentPublicKey: PublicKey, @@ -6055,4 +6125,35 @@ export class AdminClient extends DriftClient { } ); } + + public async updatePerpMarketLpPoolPausedOperations( + marketIndex: number, + pausedOperations: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex, + pausedOperations + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex: number, + pausedOperations: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolPausedOperations( + pausedOperations, + { + accounts: { + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 88ea1a2583..6a1cee63d7 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4730,6 +4730,32 @@ } ] }, + { + "name": "updatePerpMarketLpPoolPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPausedOperations", + "type": "u8" + } + ] + }, { "name": "updatePerpMarketLpPoolStatus", "accounts": [ @@ -7946,6 +7972,58 @@ } ] }, + { + "name": "updateConstituentStatus", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "newStatus", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, { "name": "updateConstituentParams", "accounts": [ @@ -9908,12 +9986,20 @@ "name": "xi", "type": "u8" }, + { + "name": "status", + "type": "u8" + }, + { + "name": "pausedOperations", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 4 + 2 ] } } @@ -10343,23 +10429,23 @@ "type": "u8" }, { - "name": "padding1", - "type": "u16" - }, - { - "name": "lastFillPrice", - "type": "u64" + "name": "lpPausedOperations", + "type": "u8" }, { "name": "lpExchangeFeeExcluscionScalar", "type": "u8" }, + { + "name": "lastFillPrice", + "type": "u64" + }, { "name": "padding", "type": { "array": [ "u8", - 23 + 24 ] } } @@ -14871,6 +14957,20 @@ ] } }, + { + "name": "ConstituentStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ReduceOnly" + }, + { + "name": "Decommissioned" + } + ] + } + }, { "name": "WeightValidationFlags", "type": { @@ -15111,6 +15211,37 @@ ] } }, + { + "name": "PerpLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TrackAmmRevenue" + }, + { + "name": "SettleQuoteOwed" + } + ] + } + }, + { + "name": "ConstituentLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Swap" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, { "name": "MarketStatus", "type": { @@ -15146,6 +15277,23 @@ ] } }, + { + "name": "LpStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Uncollateralized" + }, + { + "name": "Active" + }, + { + "name": "Decommissioning" + } + ] + } + }, { "name": "ContractType", "type": { @@ -18884,6 +19032,11 @@ "code": 6335, "name": "LpPoolSettleInvariantBreached", "msg": "Settlement amount exceeded" + }, + { + "code": 6336, + "name": "InvalidConstituentOperation", + "msg": "Invalid constituent operation" } ], "metadata": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 7b6fb37189..42462b5465 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1695,6 +1695,17 @@ export type InitializeConstituentParams = { xi?: number; }; +export enum ConstituentStatus { + ACTIVE = 0, + REDUCE_ONLY = 1, + DECOMMISSIONED = 2, +} +export enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + export type ConstituentAccount = { pubkey: PublicKey; spotMarketIndex: number; @@ -1718,6 +1729,8 @@ export type ConstituentAccount = { nextSwapId: BN; derivativeWeight: BN; flashLoanInitialTokenAmount: BN; + status: number; + pausedOperations: number; }; export type CacheInfo = { diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 10aa064a67..4729330a89 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -54,6 +54,7 @@ import { SpotBalanceType, getTokenAmount, TWO, + ConstituentLpOperation, } from '../sdk/src'; import { @@ -619,6 +620,36 @@ describe('LP Pool', () => { } }); + it('fails to add liquidity if a paused operation', async () => { + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + ConstituentLpOperation.Deposit + ); + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + 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); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + 0 + ); + }); + it('can update pool aum', async () => { let lpPool = (await adminClient.program.account.lpPool.fetch( lpPoolKey @@ -920,20 +951,20 @@ describe('LP Pool', () => { // 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; + // Test pausing tracking for market 0 + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 1); 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].lastFeePoolTokenAmount.eq(ZERO)); assert( ammCache.cache[0].quoteOwedFromLpPool.eq( - ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool.sub( - new BN(75).mul(QUOTE_PRECISION) - ) + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool ) ); + assert(ammCache.cache[1].lastFeePoolTokenAmount.eq(new BN(100000000))); assert( ammCache.cache[1].quoteOwedFromLpPool.eq( ammCacheBeforeAdjust.cache[1].quoteOwedFromLpPool.sub( @@ -942,6 +973,21 @@ describe('LP Pool', () => { ) ); + // Market 0 on the amm cache will update now that tracking is permissioned again + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 0); + 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[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool.sub( + new BN(75).mul(QUOTE_PRECISION) + ) + ) + ); + const usdcBefore = constituent.vaultTokenBalance; // Update Amm Cache to update the aum await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts index d22ba141c2..db0fc37fca 100644 --- a/tests/lpPoolCUs.ts +++ b/tests/lpPoolCUs.ts @@ -523,7 +523,7 @@ describe('LP Pool', () => { } }); - it('can add all addresses to lookup tables', async () => { + it('can add all addresses to lookup tables', async () => { const slot = new BN( await bankrunContextWrapper.connection.toConnection().getSlot() ); @@ -536,17 +536,22 @@ describe('LP Pool', () => { }); const extendInstruction = AddressLookupTableProgram.extendLookupTable({ - payer: adminClient.wallet.publicKey, - authority: adminClient.wallet.publicKey, - lookupTable: lookupTableAddress, - addresses: CONSTITUENT_INDEXES.map((i) => getConstituentPublicKey(program.programId, lpPoolKey, i)), + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: CONSTITUENT_INDEXES.map((i) => + getConstituentPublicKey(program.programId, lpPoolKey, i) + ), }); const tx = new Transaction().add(lookupTableInst).add(extendInstruction); await adminClient.sendTransaction(tx); lutAddress = lookupTableAddress; - const chunkies = chunks(adminClient.getPerpMarketAccounts().map((account) => account.pubkey), 20); + const chunkies = chunks( + adminClient.getPerpMarketAccounts().map((account) => account.pubkey), + 20 + ); for (const chunk of chunkies) { const extendTx = new Transaction(); const extendInstruction = AddressLookupTableProgram.extendLookupTable({ @@ -567,7 +572,8 @@ describe('LP Pool', () => { for (const chunk of chunks(PERP_MARKET_INDEXES, 20)) { const txSig = await adminClient.updateAmmCache(chunk); - const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); + const cus = + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); assert(cus < 200_000); } @@ -577,7 +583,6 @@ describe('LP Pool', () => { )) as AmmCache; expect(ammCache).to.not.be.null; assert(ammCache.cache.length == NUMBER_OF_PERP_MARKETS); - }); it('can update target balances', async () => { @@ -592,9 +597,11 @@ describe('LP Pool', () => { const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000, }); - const ammCacheIxs = await Promise.all(chunks(PERP_MARKET_INDEXES, 50).map(async (chunk) => await adminClient.getUpdateAmmCacheIx( - chunk - ))); + const ammCacheIxs = await Promise.all( + chunks(PERP_MARKET_INDEXES, 50).map( + async (chunk) => await adminClient.getUpdateAmmCacheIx(chunk) + ) + ); const updateBaseIx = await adminClient.getUpdateLpConstituentTargetBaseIx( encodeName(lpPoolName), [getConstituentPublicKey(program.programId, lpPoolKey, 1)] diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts index 8e660535a7..f6abd65f08 100644 --- a/tests/lpPoolSwap.ts +++ b/tests/lpPoolSwap.ts @@ -31,6 +31,7 @@ import { getSerumSignerPublicKey, BN_MAX, isVariant, + ConstituentStatus, getSignedTokenAmount, getTokenAmount, MAX_LEVERAGE_ORDER_SIZE, @@ -573,7 +574,7 @@ describe('LP Pool', () => { ); const tokensAdded = new BN(1_000_000_000_000); - const tx = new Transaction(); + let tx = new Transaction(); tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); tx.add( ...(await adminClient.getLpPoolAddLiquidityIx({ @@ -585,6 +586,31 @@ describe('LP Pool', () => { ); await adminClient.sendTransaction(tx); + // Should fail to add more liquidity if it's in redulce only mode; + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.REDUCE_ONLY + ); + 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, + })) + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.ACTIVE + ); + const userC0TokenBalanceAfter = await bankrunContextWrapper.connection.getTokenAccount( c0UserTokenAccount @@ -867,6 +893,22 @@ describe('LP Pool', () => { .add(settleFundsIx) .add(endSwapIx); + // Should fail if usdc is in reduce only + const c0pubkey = getConstituentPublicKey(program.programId, lpPoolKey, 0); + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.REDUCE_ONLY + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.ACTIVE + ); + const { txSig } = await adminClient.sendTransaction(tx); bankrunContextWrapper.printTxLogs(txSig);