diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs index ba0356284d..c46d5ac234 100644 --- a/programs/drift/src/instructions/lp_pool.rs +++ b/programs/drift/src/instructions/lp_pool.rs @@ -43,7 +43,6 @@ use crate::state::lp_pool::{ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, - constituent_indexes: Vec, ) -> Result<()> { let lp_pool = &ctx.accounts.lp_pool.load()?; let lp_pool_key: &Pubkey = &ctx.accounts.lp_pool.key(); @@ -97,16 +96,6 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( msg!("weight datum: {:?}", datum); } - let exists_invalid_constituent_index = constituent_indexes - .iter() - .any(|index| *index as u32 >= num_constituents); - - validate!( - !exists_invalid_constituent_index, - ErrorCode::InvalidUpdateConstituentTargetBaseArgument, - "Constituent index larger than number of constituent target weights" - )?; - let slot = Clock::get()?.slot; let amm_constituent_mapping: AccountZeroCopy< @@ -131,8 +120,7 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( "Amm mapping PDA does not match expected PDA" )?; - let mut amm_inventories: Vec<(u16, i64)> = vec![]; - let mut oracle_prices: Vec = vec![]; + let mut amm_inventories: Vec<(u16, i64, i64)> = vec![]; for (_, datum) in amm_constituent_mapping.iter().enumerate() { let cache_info = amm_cache.get(datum.perp_market_index as u32); @@ -161,8 +149,11 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( continue; } - amm_inventories.push((datum.perp_market_index, cache_info.position)); - oracle_prices.push(cache_info.oracle_price); + amm_inventories.push(( + datum.perp_market_index, + cache_info.position, + cache_info.oracle_price, + )); } if amm_inventories.is_empty() { @@ -170,10 +161,30 @@ pub fn handle_update_constituent_target_base<'c: 'info, 'info>( return Ok(()); } + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; + + let mut constituent_indexes_and_prices: Vec<(u16, i64)> = vec![]; + for (index, loader) in &constituent_map.0 { + let constituent_ref = loader.load()?; + constituent_indexes_and_prices.push((*index, constituent_ref.last_oracle_price)); + } + + let exists_invalid_constituent_index = constituent_indexes_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.as_slice(), + constituent_indexes_and_prices.as_slice(), slot, )?; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 911b5d2acd..42d0be9490 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1792,9 +1792,8 @@ pub mod drift { pub fn update_lp_constituent_target_base<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, - constituent_indexes: Vec, ) -> Result<()> { - handle_update_constituent_target_base(ctx, constituent_indexes) + handle_update_constituent_target_base(ctx) } pub fn update_lp_pool_aum<'c: 'info, 'info>( diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 3f29f1469e..4fee3aee83 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -1,7 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ - BASE_PRECISION_I64, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, QUOTE_PRECISION, + BASE_PRECISION_I128, BASE_PRECISION_I64, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + QUOTE_PRECISION, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; @@ -565,7 +566,7 @@ impl Constituent { // } #[zero_copy] -#[derive(Debug, Default, BorshDeserialize, BorshSerialize)] +#[derive(Debug, BorshDeserialize, BorshSerialize)] #[repr(C)] pub struct AmmConstituentDatum { pub perp_market_index: u16, @@ -576,6 +577,18 @@ pub struct AmmConstituentDatum { 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)] @@ -745,13 +758,15 @@ pub fn calculate_target_weight( lp_pool_aum: u128, validation_flags: WeightValidationFlags, ) -> DriftResult { - let notional = target_base.safe_mul(price)?.safe_div(BASE_PRECISION_I64)?; + let notional: i128 = (target_base as i128) + .safe_mul(price as i128)? + .safe_div(BASE_PRECISION_I128)?; let target_weight = notional - .cast::()? .safe_mul(PERCENTAGE_PRECISION_I128)? .safe_div(lp_pool_aum.cast::()?)? - .cast::()?; + .cast::()? + .clamp(-1 * PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); // if (validation_flags as u8 & (WeightValidationFlags::NoNegativeWeights as u8) != 0) // && target_weight < 0 @@ -781,37 +796,57 @@ impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { &mut self, mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, // (perp market index, inventory, price) - amm_inventory: &[(u16, i64)], - constituents_indexes: &[u16], + amm_inventory_and_prices: &[(u16, i64, i64)], + constituents_indexes_and_prices: &[(u16, i64)], slot: u64, ) -> DriftResult> { - let mut results = Vec::with_capacity(constituents_indexes.len()); - - for (i, constituent_index) in constituents_indexes.iter().enumerate() { - let mut target_amount = 0i128; - - for (perp_market_index, inventory) in amm_inventory.iter() { - let idx = mapping - .iter() - .position(|d| &d.perp_market_index == perp_market_index) - .expect("missing mapping for this market index"); - let weight = mapping.get(idx as u32).weight; // PERCENTAGE_PRECISION - - target_amount += (*inventory as i128) + let mut results = Vec::with_capacity(constituents_indexes_and_prices.len()); + for (i, constituent_index_and_price) in constituents_indexes_and_prices.iter().enumerate() { + let mut target_notional = 0i128; + let constituent_index = constituent_index_and_price.0; + let price = constituent_index_and_price.1; + + 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_I64 as i128); + .saturating_div(PERCENTAGE_PRECISION_I128); } let cell = self.get_mut(i as u32); + let target_base = target_notional + .safe_mul(BASE_PRECISION_I128)? + .safe_div(price as i128)? + * -1; // Want to target opposite sign of total scaled notional inventory + msg!( - "updating constituent index {} target amount to {}", + "updating constituent index {} target base to {} from target notional {}", constituent_index, - target_amount + target_base, + target_notional, ); - cell.target_base = target_amount as i64; + cell.target_base = target_base.cast::()?; cell.last_slot = slot; - results.push(target_amount); + results.push(target_base); } Ok(results) @@ -825,7 +860,9 @@ impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> 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 { + 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() { diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index d7e38f532d..db78aec576 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use crate::math::constants::PERCENTAGE_PRECISION_I64; + use crate::math::constants::{PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64}; use crate::state::lp_pool::*; use std::{cell::RefCell, marker::PhantomData, vec}; @@ -19,6 +19,114 @@ mod tests { } } + #[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 constituent_indexes_and_prices = vec![ + (0, 100_000 * PRICE_PRECISION_I64), + (1, 200 * PRICE_PRECISION_I64), + (2, 1500 * PRICE_PRECISION_I64), + (3, 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, + &constituent_indexes_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(), + 0, + &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); @@ -47,9 +155,8 @@ mod tests { } }; - let amm_inventory: Vec<(u16, i64)> = vec![(0, 1_000_000)]; - let prices = vec![1_000_000]; - let constituent_indexes = vec![1]; + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000, 1_000_000)]; + let constituent_indexes_and_prices = vec![(1, 1_000_000)]; let aum = 1_000_000; let now_ts = 1000; @@ -65,7 +172,12 @@ mod tests { }; let totalw = target_zc_mut - .update_target_base(&mapping_zc, &amm_inventory, &constituent_indexes, now_ts) + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituent_indexes_and_prices, + now_ts, + ) .unwrap(); assert!(totalw.iter().all(|&x| x == 0)); @@ -102,9 +214,9 @@ mod tests { } }; - let amm_inventory = vec![(0, 1_000_000)]; - let prices = vec![1_000_000]; - let constituent_indexes = [1u16]; + let price = PRICE_PRECISION_I64; + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, BASE_PRECISION_I64, price)]; + let constituent_indexes_and_prices = vec![(1, price)]; let aum = 1_000_000; let now_ts = 1234; @@ -119,14 +231,28 @@ mod tests { _marker: PhantomData::, }; - let totalw = target_zc_mut - .update_target_base(&mapping_zc, &amm_inventory, &constituent_indexes, now_ts) + let base = target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituent_indexes_and_prices, + now_ts, + ) .unwrap(); - assert_eq!(totalw, [1000000]); - + let weight = calculate_target_weight( + *base.get(0).unwrap() as i64, + 0, + &SpotMarket::default(), + price, + aum, + WeightValidationFlags::NONE, + ) + .unwrap(); + + assert_eq!(*base.get(0).unwrap(), -1 * BASE_PRECISION_I128); + assert_eq!(weight, -1000000); assert_eq!(target_zc_mut.len(), 1); - assert_eq!(target_zc_mut.get(0).target_base, PERCENTAGE_PRECISION_I64); assert_eq!(target_zc_mut.get(0).last_slot, now_ts); } @@ -169,9 +295,9 @@ mod tests { } }; - let amm_inventory = vec![(0, 1_000_000)]; - let prices = vec![1_000_000, 1_000_000]; - let constituent_indexes = vec![1, 2]; + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000, 1_000_000)]; + let constituent_indexes_and_prices = vec![(1, 1_000_000), (2, 1_000_000)]; + let aum = 1_000_000; let now_ts = 999; @@ -187,7 +313,12 @@ mod tests { }; target_zc_mut - .update_target_base(&mapping_zc, &amm_inventory, &constituent_indexes, now_ts) + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituent_indexes_and_prices, + now_ts, + ) .unwrap(); assert_eq!(target_zc_mut.len(), 2); @@ -195,7 +326,7 @@ mod tests { for i in 0..target_zc_mut.len() { assert_eq!( target_zc_mut.get(i).target_base, - PERCENTAGE_PRECISION_I64 / 2 + -1 * PERCENTAGE_PRECISION_I64 / 2 ); assert_eq!(target_zc_mut.get(i).last_slot, now_ts); } @@ -229,9 +360,10 @@ mod tests { } }; - let amm_inventory = vec![(0, 1_000_000)]; + let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000, 142_000_000)]; + let constituent_indexes_and_prices = vec![(1, 142_000_000)]; + let prices = vec![142_000_000]; - let constituent_indexes = vec![1u16]; let aum = 0; let now_ts = 111; @@ -247,65 +379,16 @@ mod tests { }; target_zc_mut - .update_target_base(&mapping_zc, &amm_inventory, &constituent_indexes, now_ts) - .unwrap(); - - assert_eq!(target_zc_mut.len(), 1); - assert_eq!(target_zc_mut.get(0).target_base, 1_000_000); // despite no aum, desire to reach target - assert_eq!(target_zc_mut.get(0).last_slot, now_ts); - } - - #[test] - fn test_overflow_protection() { - let amm_datum = amm_const_datum(0, 1, i64::MAX, 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 = vec![(0, i64::MAX)]; - let prices = vec![i64::MAX]; - let constituent_indexes = vec![1u16]; - let aum = 1; - let now_ts = 222; - - 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, &constituent_indexes, now_ts) + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + &constituent_indexes_and_prices, + now_ts, + ) .unwrap(); assert_eq!(target_zc_mut.len(), 1); - assert!(target_zc_mut.get(0).target_base < i64::MAX); // rounding sat div + assert_eq!(target_zc_mut.get(0).target_base, -1_000_000); // despite no aum, desire to reach target assert_eq!(target_zc_mut.get(0).last_slot, now_ts); } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 46c1387025..2b75493f29 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -9714,15 +9714,12 @@ export class DriftClient { public async updateLpConstituentTargetBase( lpPoolName: number[], - constituentIndexesToUpdate: number[], + constituents: PublicKey[], txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( - await this.getUpdateLpConstituentTargetBaseIx( - lpPoolName, - constituentIndexesToUpdate - ), + await this.getUpdateLpConstituentTargetBaseIx(lpPoolName, constituents), txParams ), [], @@ -9733,7 +9730,7 @@ export class DriftClient { public async getUpdateLpConstituentTargetBaseIx( lpPoolName: number[], - constituentIndexesToUpdate: number[] + constituents: PublicKey[] ): Promise { const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( @@ -9747,19 +9744,25 @@ export class DriftClient { const ammCache = getAmmCachePublicKey(this.program.programId); - return this.program.instruction.updateLpConstituentTargetBase( - constituentIndexesToUpdate, - { - accounts: { - keeper: this.wallet.publicKey, - lpPool, - ammConstituentMapping: ammConstituentMappingPublicKey, - constituentTargetBase, - state: await this.getStatePublicKey(), - ammCache, - }, - } - ); + 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( diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 1270c75ab7..71afd4aa59 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7587,14 +7587,7 @@ "isSigner": false } ], - "args": [ - { - "name": "constituentIndexes", - "type": { - "vec": "u16" - } - } - ] + "args": [] }, { "name": "updateLpPoolAum", diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 1bc4877063..7cc88a4311 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -312,7 +312,7 @@ describe('LP Pool', () => { assert(ammMapping.weights.length == 2); }); - it('can update constituent beta and cost to trade', async () => { + it('can update constituent properties', async () => { const constituentPublicKey = getConstituentPublicKey( program.programId, lpPoolKey, @@ -415,7 +415,7 @@ describe('LP Pool', () => { }); await adminClient.updateLpConstituentTargetBase(encodeName(lpPoolName), [ - 0, + getConstituentPublicKey(program.programId, lpPoolKey, 0), ]); const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( program.programId,