Skip to content

Commit bd615ea

Browse files
slot staleness checks (#1705)
* slot staleness checks * update aum ix to use constituent oracles
1 parent 30fe08f commit bd615ea

File tree

10 files changed

+325
-165
lines changed

10 files changed

+325
-165
lines changed

programs/drift/src/error.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
use anchor_lang::prelude::*;
2-
3-
use crate::state::lp_pool::Constituent;
4-
52
pub type DriftResult<T = ()> = std::result::Result<T, ErrorCode>;
63

74
#[error_code]
@@ -659,6 +656,12 @@ pub enum ErrorCode {
659656
OracleTooStaleForLPAUMUpdate,
660657
#[msg("Insufficient constituent token balance")]
661658
InsufficientConstituentTokenBalance,
659+
#[msg("Amm Cache data too stale")]
660+
AMMCacheStale,
661+
#[msg("LP Pool AUM not updated recently")]
662+
LpPoolAumDelayed,
663+
#[msg("Constituent oracle is stale")]
664+
ConstituentOracleStale,
662665
}
663666

664667
#[macro_export]

programs/drift/src/instructions/keeper.rs

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use crate::state::insurance_fund_stake::InsuranceFundStake;
5050
use crate::state::lp_pool::Constituent;
5151
use crate::state::lp_pool::LPPool;
5252
use crate::state::lp_pool::CONSTITUENT_PDA_SEED;
53+
use crate::state::lp_pool::SETTLE_AMM_ORACLE_MAX_DELAY;
5354
use crate::state::oracle_map::OracleMap;
5455
use crate::state::order_params::{OrderParams, PlaceOrderOptions};
5556
use crate::state::paused_operations::{PerpOperation, SpotOperation};
@@ -2949,6 +2950,8 @@ pub fn handle_pause_spot_market_deposit_withdraw(
29492950
pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>(
29502951
ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>,
29512952
) -> Result<()> {
2953+
let slot = Clock::get()?.slot;
2954+
29522955
let state = &ctx.accounts.state;
29532956
let amm_cache_key = &ctx.accounts.amm_cache.key();
29542957
let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> =
@@ -2978,19 +2981,27 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>(
29782981
let AccountMaps {
29792982
perp_market_map,
29802983
spot_market_map: _,
2981-
mut oracle_map,
2984+
oracle_map: _,
29822985
} = load_maps(
29832986
remaining_accounts_iter,
29842987
&MarketSet::new(),
29852988
&MarketSet::new(),
2986-
Clock::get()?.slot,
2989+
slot,
29872990
None,
29882991
)?;
29892992

29902993
for (_, perp_market_loader) in perp_market_map.0.iter() {
29912994
let mut perp_market = perp_market_loader.load_mut()?;
29922995
let cached_info = amm_cache.get_mut(perp_market.market_index as u32);
2993-
let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?;
2996+
2997+
if slot.saturating_sub(cached_info.oracle_slot) > SETTLE_AMM_ORACLE_MAX_DELAY {
2998+
// If the oracle slot is not up to date, skip this market
2999+
msg!(
3000+
"Skipping settling perp market {} to dlp because oracle slot is not up to date",
3001+
perp_market.market_index
3002+
);
3003+
continue;
3004+
}
29943005

29953006
let fee_pool_token_amount = get_token_amount(
29963007
perp_market.amm.fee_pool.scaled_balance,
@@ -3004,7 +3015,10 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>(
30043015
perp_market.pnl_pool.balance_type(),
30053016
)?
30063017
.cast::<i128>()?
3007-
.safe_sub(calculate_net_user_pnl(&perp_market.amm, oracle_data.price)?)?;
3018+
.safe_sub(calculate_net_user_pnl(
3019+
&perp_market.amm,
3020+
cached_info.oracle_price,
3021+
)?)?;
30083022

30093023
let amm_amount_available =
30103024
net_pnl_pool_token_amount.safe_add(fee_pool_token_amount.cast::<i128>()?)?;
@@ -3024,53 +3038,7 @@ pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>(
30243038
)?;
30253039

30263040
// Actually transfer the pnl to the lp usdc constituent account
3027-
let oracle_price = oracle_map.get_price_data(&perp_market.oracle_id())?.price;
3028-
validate_market_within_price_band(&perp_market, state, oracle_price)?;
3029-
3030-
if perp_market.amm.curve_update_intensity > 0 {
3031-
let healthy_oracle = perp_market.amm.is_recent_oracle_valid(oracle_map.slot)?;
3032-
3033-
if !healthy_oracle {
3034-
let (_, oracle_validity) = oracle_map.get_price_data_and_validity(
3035-
MarketType::Perp,
3036-
perp_market.market_index,
3037-
&perp_market.oracle_id(),
3038-
perp_market
3039-
.amm
3040-
.historical_oracle_data
3041-
.last_oracle_price_twap,
3042-
perp_market.get_max_confidence_interval_multiplier()?,
3043-
0,
3044-
)?;
3045-
3046-
if !is_oracle_valid_for_action(oracle_validity, Some(DriftAction::SettlePnl))?
3047-
|| !perp_market.is_price_divergence_ok_for_settle_pnl(oracle_price)?
3048-
{
3049-
if !perp_market.amm.last_oracle_valid {
3050-
let msg = format!(
3051-
"Oracle Price detected as invalid ({}) on last perp market update for Market = {}",
3052-
oracle_validity,
3053-
perp_market.market_index
3054-
);
3055-
msg!(&msg);
3056-
3057-
return Err(oracle_validity.get_error_code().into());
3058-
}
3059-
3060-
if oracle_map.slot != perp_market.amm.last_update_slot {
3061-
let msg = format!(
3062-
"Market={} AMM must be updated in a prior instruction within same slot (current={} != amm={}, last_oracle_valid={})",
3063-
perp_market.market_index,
3064-
oracle_map.slot,
3065-
perp_market.amm.last_update_slot,
3066-
perp_market.amm.last_oracle_valid
3067-
);
3068-
msg!(&msg);
3069-
return Err(ErrorCode::AMMNotUpdatedInSameSlot.into());
3070-
}
3071-
}
3072-
}
3073-
}
3041+
validate_market_within_price_band(&perp_market, state, cached_info.oracle_price)?;
30743042

30753043
if perp_market.is_operation_paused(PerpOperation::SettlePnl) {
30763044
msg!(

programs/drift/src/instructions/lp_pool.rs

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use crate::{
3030
calculate_target_weight, AmmConstituentDatum, AmmConstituentMappingFixed, Constituent,
3131
ConstituentCorrelationsFixed, ConstituentTargetBaseFixed, LPPool, TargetsDatum,
3232
WeightValidationFlags, CONSTITUENT_CORRELATIONS_PDA_SEED,
33+
LP_POOL_SWAP_AUM_UPDATE_DELAY, MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC,
34+
MAX_CONSTITUENT_ORACLE_SLOT_STALENESS_FOR_AUM,
3335
},
3436
oracle::OraclePriceData,
3537
oracle_map::OracleMap,
@@ -58,13 +60,18 @@ use crate::state::lp_pool::{
5860
pub fn handle_update_constituent_target_base<'c: 'info, 'info>(
5961
ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>,
6062
) -> Result<()> {
63+
let slot = Clock::get()?.slot;
64+
6165
let lp_pool = &ctx.accounts.lp_pool.load()?;
6266
let lp_pool_key: &Pubkey = &ctx.accounts.lp_pool.key();
6367
let amm_cache_key: &Pubkey = &ctx.accounts.amm_cache.key();
6468

6569
let amm_cache: AccountZeroCopy<'_, CacheInfo, AmmCacheFixed> =
6670
ctx.accounts.amm_cache.load_zc()?;
6771

72+
amm_cache.check_oracle_staleness(slot, MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC)?;
73+
amm_cache.check_perp_market_staleness(slot, MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC)?;
74+
6875
let expected_cache_pda = &Pubkey::create_program_address(
6976
&[
7077
AMM_POSITIONS_CACHE.as_ref(),
@@ -223,7 +230,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
223230
let AccountMaps {
224231
perp_market_map: _,
225232
spot_market_map,
226-
mut oracle_map,
233+
oracle_map: _,
227234
} = load_maps(
228235
remaining_accounts,
229236
&MarketSet::new(),
@@ -284,7 +291,19 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
284291
let mut oldest_slot = u64::MAX;
285292
let mut derivative_groups: BTreeMap<u16, Vec<u16>> = BTreeMap::new();
286293
for i in 0..lp_pool.constituents as usize {
287-
let mut constituent = constituent_map.get_ref_mut(&(i as u16))?;
294+
let constituent = constituent_map.get_ref(&(i as u16))?;
295+
if slot.saturating_sub(constituent.last_oracle_slot)
296+
> MAX_CONSTITUENT_ORACLE_SLOT_STALENESS_FOR_AUM
297+
{
298+
msg!(
299+
"Constituent {} oracle slot is too stale: {}, current slot: {}",
300+
constituent.constituent_index,
301+
constituent.last_oracle_slot,
302+
slot
303+
);
304+
return Err(ErrorCode::ConstituentOracleStale.into());
305+
}
306+
288307
if constituent.constituent_derivative_index >= 0 && constituent.derivative_weight != 0 {
289308
if !derivative_groups.contains_key(&(constituent.constituent_derivative_index as u16)) {
290309
derivative_groups.insert(
@@ -301,40 +320,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
301320

302321
let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?;
303322

304-
let oracle_data = oracle_map.get_price_data_and_validity(
305-
MarketType::Spot,
306-
constituent.spot_market_index,
307-
&spot_market.oracle_id(),
308-
spot_market.historical_oracle_data.last_oracle_price_twap,
309-
spot_market.get_max_confidence_interval_multiplier()?,
310-
0,
311-
)?;
312-
313-
let oracle_slot = slot - oracle_data.0.delay.max(0i64).cast::<u64>()?;
314-
let oracle_price: Option<i64> = {
315-
if !is_oracle_valid_for_action(oracle_data.1, Some(DriftAction::UpdateLpPoolAum))? {
316-
msg!(
317-
"Oracle data for spot market {} is invalid. Skipping update",
318-
spot_market.market_index,
319-
);
320-
if slot.saturating_sub(constituent.last_oracle_slot)
321-
>= constituent.oracle_staleness_threshold
322-
{
323-
None
324-
} else {
325-
Some(constituent.last_oracle_price)
326-
}
327-
} else {
328-
Some(oracle_data.0.price)
329-
}
330-
};
331-
332-
if oracle_price.is_none() {
333-
return Err(ErrorCode::OracleTooStaleForLPAUMUpdate.into());
334-
}
335-
336-
constituent.last_oracle_price = oracle_price.unwrap();
337-
constituent.last_oracle_slot = oracle_slot;
323+
let oracle_slot = constituent.last_oracle_slot;
338324

339325
if oracle_slot < oldest_slot {
340326
oldest_slot = oracle_slot;
@@ -350,7 +336,7 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
350336
.get_full_balance(&spot_market)?
351337
.safe_mul(numerator_scale)?
352338
.safe_div(denominator_scale)?
353-
.safe_mul(oracle_price.unwrap() as i128)?
339+
.safe_mul(constituent.last_oracle_price as i128)?
354340
.safe_div(PRICE_PRECISION_I128)?
355341
.max(0);
356342
msg!(
@@ -366,15 +352,15 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
366352
.get(constituent.constituent_index as u32)
367353
.target_base
368354
.safe_mul(constituent.last_oracle_price)?
369-
.safe_div(10_i64.pow(spot_market.decimals as u32))?;
355+
.safe_div(10_i64.pow(constituent.decimals as u32))?;
370356
crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?;
371357
}
372358
aum = aum.safe_add(constituent_aum.cast()?)?;
373359
}
374360

375361
for cache_datum in amm_cache.iter() {
376362
if cache_datum.quote_owed_from_lp > 0 {
377-
aum = aum.safe_sub(cache_datum.quote_owed_from_lp.abs().cast::<u128>()?)?;
363+
aum = aum.saturating_sub(cache_datum.quote_owed_from_lp.abs().cast::<u128>()?);
378364
} else {
379365
aum = aum.safe_add(cache_datum.quote_owed_from_lp.abs().cast::<u128>()?)?;
380366
}
@@ -487,6 +473,15 @@ pub fn handle_lp_pool_swap<'c: 'info, 'info>(
487473
let state = &ctx.accounts.state;
488474
let lp_pool = &ctx.accounts.lp_pool.load()?;
489475

476+
if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY {
477+
msg!(
478+
"Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}",
479+
lp_pool.last_aum_slot,
480+
slot
481+
);
482+
return Err(ErrorCode::LpPoolAumDelayed.into());
483+
}
484+
490485
let mut in_constituent = ctx.accounts.in_constituent.load_mut()?;
491486
let mut out_constituent = ctx.accounts.out_constituent.load_mut()?;
492487

@@ -729,6 +724,15 @@ pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>(
729724
let state = &ctx.accounts.state;
730725
let mut lp_pool = ctx.accounts.lp_pool.load_mut()?;
731726

727+
if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY {
728+
msg!(
729+
"Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}",
730+
lp_pool.last_aum_slot,
731+
slot
732+
);
733+
return Err(ErrorCode::LpPoolAumDelayed.into());
734+
}
735+
732736
let mut in_constituent = ctx.accounts.in_constituent.load_mut()?;
733737

734738
let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?;
@@ -917,6 +921,15 @@ pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>(
917921
let state = &ctx.accounts.state;
918922
let mut lp_pool = ctx.accounts.lp_pool.load_mut()?;
919923

924+
if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY {
925+
msg!(
926+
"Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}",
927+
lp_pool.last_aum_slot,
928+
slot
929+
);
930+
return Err(ErrorCode::LpPoolAumDelayed.into());
931+
}
932+
920933
let mut out_constituent = ctx.accounts.out_constituent.load_mut()?;
921934

922935
let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?;

programs/drift/src/state/lp_pool.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ pub const MIN_SWAP_FEE: i128 = 200; // 0.75% in PERCENTAGE_PRECISION
3131

3232
pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000;
3333

34+
// Delay constants
35+
#[cfg(feature = "anchor-test")]
36+
pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 100;
37+
#[cfg(not(feature = "anchor-test"))]
38+
pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 10;
39+
pub const LP_POOL_SWAP_AUM_UPDATE_DELAY: u64 = 0;
40+
#[cfg(feature = "anchor-test")]
41+
pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64;
42+
#[cfg(not(feature = "anchor-test"))]
43+
pub const MAX_AMM_CACHE_STALENESS_FOR_TARGET_CALC: u64 = 0u64;
44+
45+
#[cfg(feature = "anchor-test")]
46+
pub const MAX_CONSTITUENT_ORACLE_SLOT_STALENESS_FOR_AUM: u64 = 10000u64;
47+
#[cfg(not(feature = "anchor-test"))]
48+
pub const MAX_CONSTITUENT_ORACLE_SLOT_STALENESS_FOR_AUM: u64 = 2u64;
49+
3450
#[cfg(test)]
3551
mod tests;
3652

@@ -1090,6 +1106,9 @@ pub fn calculate_target_weight(
10901106
lp_pool_aum: u128,
10911107
validation_flags: WeightValidationFlags,
10921108
) -> DriftResult<i64> {
1109+
if lp_pool_aum == 0 {
1110+
return Ok(0);
1111+
}
10931112
let notional: i128 = (target_base as i128)
10941113
.safe_mul(price as i128)?
10951114
.safe_div(10_i128.pow(spot_market.decimals))?;

programs/drift/src/state/perp_market.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::state::pyth_lazer_oracle::PythLazerOracle;
2+
use crate::state::zero_copy::AccountZeroCopy;
23
use crate::{impl_zero_copy_loader, validate};
34
use anchor_lang::prelude::*;
45

@@ -1807,3 +1808,40 @@ impl AmmCache {
18071808
}
18081809

18091810
impl_zero_copy_loader!(AmmCache, crate::id, AmmCacheFixed, CacheInfo);
1811+
1812+
impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> {
1813+
pub fn check_settle_staleness(&self, now: i64, threshold_ms: i64) -> DriftResult<()> {
1814+
for (i, cache_info) in self.iter().enumerate() {
1815+
if cache_info.last_settle_ts < now.saturating_sub(threshold_ms) {
1816+
msg!("AMM settle data is stale for perp market {}", i);
1817+
return Err(ErrorCode::AMMCacheStale.into());
1818+
}
1819+
}
1820+
Ok(())
1821+
}
1822+
1823+
pub fn check_perp_market_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> {
1824+
for (i, cache_info) in self.iter().enumerate() {
1825+
if cache_info.slot < slot.saturating_sub(threshold) {
1826+
msg!("Perp market cache info is stale for perp market {}", i);
1827+
return Err(ErrorCode::AMMCacheStale.into());
1828+
}
1829+
}
1830+
Ok(())
1831+
}
1832+
1833+
pub fn check_oracle_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> {
1834+
for (i, cache_info) in self.iter().enumerate() {
1835+
if cache_info.oracle_slot < slot.saturating_sub(threshold) {
1836+
msg!(
1837+
"Perp market cache info is stale for perp market {}. oracle slot: {}, slot: {}",
1838+
i,
1839+
cache_info.oracle_slot,
1840+
slot
1841+
);
1842+
return Err(ErrorCode::AMMCacheStale.into());
1843+
}
1844+
}
1845+
Ok(())
1846+
}
1847+
}

0 commit comments

Comments
 (0)