diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 8cd95ad9dc..640ba7d294 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -635,8 +635,8 @@ pub enum ErrorCode { InvalidSignedMsgUserOrdersResize, #[msg("Invalid Constituent")] InvalidConstituent, - #[msg("Misatch amm mapping and constituent target weights")] - MismatchAmmConstituentMappingAndConstituentTargetWeights, + #[msg("Invalid Amm Constituent Mapping argument")] + InvalidAmmConstituentMappingArgument, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 7d56ab234e..81bcd2d358 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4600,6 +4600,62 @@ pub fn handle_initialize_constituent<'info>( Ok(()) } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct InitializeAmmConstituentMappingDatum { + pub constituent_index: u16, + pub perp_market_index: u16, +} + +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_weights = &ctx.accounts.constituent_target_weights; + let state = &ctx.accounts.state; + let mut current_len = amm_mapping.data.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_weights.data.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; + + // Check if the datum already exists + let exists = amm_mapping.data.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.data.resize(current_len, datum); + } + + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5409,8 +5465,43 @@ pub struct InitializeConstituent<'info> { payer = admin, )] pub constituent: AccountLoader<'info, Constituent>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} +#[derive(Accounts)] +#[instruction( + lp_pool_name: [u8; 32], + market_index_constituent_index_pairs: Vec<(u16, u16)>, +)] +pub struct AddAmmConstituentMappingData<'info> { #[account(mut)] - pub payer: Signer<'info>, + pub admin: Signer<'info>, + + #[account( + seeds = [b"lp_pool", lp_pool_name.as_ref()], + bump, + )] + 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.data.len() + market_index_constituent_index_pairs.len()), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_WEIGHT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = ConstituentTargetWeights::space(constituent_target_weights.data.len() + 1 as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_weights: Box>, + pub state: Box>, pub system_program: Program<'info, System>, } diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 042b005708..cbd2a4a526 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1706,6 +1706,33 @@ pub mod drift { ) -> Result<()> { handle_update_protected_maker_mode_config(ctx, max_users, reduce_only, current_users) } + + pub fn initialize_constituent( + ctx: Context, + lp_pool_name: [u8; 32], + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + ) -> Result<()> { + handle_initialize_constituent( + ctx, + spot_market_index, + decimals, + max_weight_deviation, + swap_fee_min, + swap_fee_max, + ) + } + + pub fn add_amm_constituent_mapping_data( + ctx: Context, + lp_pool_name: [u8; 32], + init_amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_add_amm_constituent_data(ctx, init_amm_constituent_mapping_data) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index 3add22c949..eec5d794e7 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -208,7 +208,7 @@ pub struct BLPosition { pub market_index: u16, /// Whether the position is deposit or borrow pub balance_type: SpotBalanceType, - pub padding: [u8; 4], + pub padding: [u8; 5], } impl SpotBalance for BLPosition { @@ -269,7 +269,7 @@ pub struct Constituent { } impl Size for Constituent { - const SIZE: usize = 108; + const SIZE: usize = 112; } impl Constituent { @@ -433,7 +433,7 @@ impl ConstituentTargetWeights { pub fn validate(&self) -> DriftResult<()> { validate!( - self.data.len() >= 0 && self.data.len() <= 128, + self.data.len() <= 128, ErrorCode::DefaultError, "Number of constituents len must be between 1 and 128" )?; diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 49e660c131..18a906e84a 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -15,6 +15,7 @@ import { ContractTier, AssetTier, SpotFulfillmentConfigStatus, + InitAmmConstituentMappingDatum, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -40,6 +41,7 @@ import { getLpPoolPublicKey, getAmmConstituentMappingPublicKey, getConstituentTargetWeightsPublicKey, + getConstituentPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; import { @@ -4272,4 +4274,111 @@ export class AdminClient extends DriftClient { createAtaIx, ]; } + + public async initializeConstituent( + lpPoolName: number[], + spotMarketIndex: number, + decimals: number, + maxWeightDeviation: BN, + swapFeeMin: BN, + swapFeeMax: BN + ): Promise { + const ixs = await this.getInitializeConstituentIx( + lpPoolName, + spotMarketIndex, + decimals, + maxWeightDeviation, + swapFeeMin, + swapFeeMax + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getInitializeConstituentIx( + lpPoolName: number[], + spotMarketIndex: number, + decimals: number, + maxWeightDeviation: BN, + swapFeeMin: BN, + swapFeeMax: BN + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const constituentTargetWeights = getConstituentTargetWeightsPublicKey( + this.program.programId, + lpPool + ); + const constituent = getConstituentPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ); + return [ + this.program.instruction.initializeConstituent( + lpPoolName, + spotMarketIndex, + decimals, + maxWeightDeviation, + swapFeeMin, + swapFeeMax, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentTargetWeights, + constituent, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }, + signers: [], + } + ), + ]; + } + + public async addInitAmmConstituentMappingData( + lpPoolName: number[], + marketIndexConstituentIndexPairs: InitAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getAddInitAmmConstituentMappingDataIx( + lpPoolName, + marketIndexConstituentIndexPairs + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getAddInitAmmConstituentMappingDataIx( + lpPoolName: number[], + marketIndexConstituentIndexPairs: InitAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolName); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetWeights = getConstituentTargetWeightsPublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.addAmmConstituentMappingData( + lpPoolName, + marketIndexConstituentIndexPairs, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + constituentTargetWeights, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index adcf9db9ec..e537792d00 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7143,6 +7143,126 @@ } } ] + }, + { + "name": "initializeConstituent", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetWeights", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolName", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "maxWeightDeviation", + "type": "i64" + }, + { + "name": "swapFeeMin", + "type": "i64" + }, + { + "name": "swapFeeMax", + "type": "i64" + } + ] + }, + { + "name": "addAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetWeights", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolName", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "initAmmConstituentMappingData", + "type": { + "vec": { + "defined": "InitializeAmmConstituentMappingDatum" + } + } + } + ] } ], "accounts": [ @@ -9315,6 +9435,22 @@ ] } }, + { + "name": "InitializeAmmConstituentMappingDatum", + "type": { + "kind": "struct", + "fields": [ + { + "name": "constituentIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + } + ] + } + }, { "name": "LiquidatePerpRecord", "type": { @@ -9581,7 +9717,7 @@ "type": { "array": [ "u8", - 4 + 5 ] } } @@ -15523,8 +15659,8 @@ }, { "code": 6315, - "name": "MismatchAmmConstituentMappingAndConstituentTargetWeights", - "msg": "Misatch amm mapping and constituent target weights" + "name": "InvalidAmmConstituentMappingArgument", + "msg": "Invalid Amm Constituent Mapping argument" } ], "metadata": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 3f22438202..2700087c91 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1472,3 +1472,40 @@ export type SignedMsgUserOrdersAccount = { authorityPubkey: PublicKey; signedMsgOrderData: SignedMsgOrderId[]; }; + +export type InitAmmConstituentMappingDatum = { + constituentIndex: number; + perpMarketIndex: number; +}; + +export type AmmConstituentDatum = InitAmmConstituentMappingDatum & { + data: BN; + lastSlot: BN; +}; + +export type AmmConstituentMapping = { + data: AmmConstituentDatum[]; +}; + +export type WeightDatum = { + data: BN; + lastSlot: BN; +}; + +export type ConstituentTargetWeights = { + data: WeightDatum[]; +}; + +export type LPPool = { + name: number[]; + pubkey: PublicKey; + mint: PublicKey; + maxAum: BN; + lastAum: BN; + lastAumSlot: BN; + lastAumTs: BN; + lastRevenueRebalanceTs: BN; + totalFeesReceived: BN; + totalFeesPaid: BN; + constituents: number; +}; diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 495f01372b..8e4c7f3561 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -1,5 +1,5 @@ import * as anchor from '@coral-xyz/anchor'; -import { expect } from 'chai'; +import { expect, assert } from 'chai'; import { Program } from '@coral-xyz/anchor'; @@ -20,9 +20,16 @@ import { getAmmConstituentMappingPublicKey, encodeName, getConstituentTargetWeightsPublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, } from '../sdk/src'; -import { initializeQuoteSpotMarket, mockUSDCMint } from './testHelpers'; +import { + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, +} from './testHelpers'; import { startAnchor } from 'solana-bankrun'; import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; @@ -37,6 +44,15 @@ describe('LP Pool', () => { let adminClient: TestClient; let usdcMint; + 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 + ); + let solUsd: PublicKey; + const lpPoolName = 'test pool 1'; const tokenName = 'test pool token'; const tokenSymbol = 'DLP-1'; @@ -98,6 +114,27 @@ describe('LP Pool', () => { await adminClient.subscribe(); await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 224.3); + const periodicity = new BN(0); + + await adminClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + + await adminClient.initializePerpMarket( + 1, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.initializeLpPool( lpPoolName, tokenName, @@ -127,17 +164,18 @@ describe('LP Pool', () => { ammConstituentMapPublicKey ); expect(ammConstituentMap).to.not.be.null; + // @ts-ignore + assert(ammConstituentMap.data.length == 0); // check constituent target weights exists - const constituentTargetWeightsPublicKey = getConstituentTargetWeightsPublicKey( - program.programId, - lpPoolKey - ); + const constituentTargetWeightsPublicKey = + getConstituentTargetWeightsPublicKey(program.programId, lpPoolKey); const constituentTargetWeights = await adminClient.program.account.constituentTargetWeights.fetch( constituentTargetWeightsPublicKey ); expect(constituentTargetWeights).to.not.be.null; + assert(constituentTargetWeights.data.length == 0); // check mint and metadata created correctly const mintAccountInfo = @@ -158,4 +196,80 @@ describe('LP Pool', () => { expect(tokenMetadata.symbol).to.equal(tokenSymbol); expect(tokenMetadata.uri).to.equal(tokenUri); }); + + it('can add constituent to LP Pool', async () => { + await adminClient.initializeConstituent( + encodeName(lpPoolName), + 0, + 6, + new BN(10).mul(PERCENTAGE_PRECISION), + new BN(1).mul(PERCENTAGE_PRECISION), + new BN(2).mul(PERCENTAGE_PRECISION) + ); + const constituentTargetWeightsPublicKey = + getConstituentTargetWeightsPublicKey(program.programId, lpPoolKey); + const constituentTargetWeights = + await adminClient.program.account.constituentTargetWeights.fetch( + constituentTargetWeightsPublicKey + ); + expect(constituentTargetWeights).to.not.be.null; + assert(constituentTargetWeights.data.length == 1); + }); + + it('can add amm mapping datum', async () => { + await adminClient.addInitAmmConstituentMappingData(encodeName(lpPoolName), [ + { + perpMarketIndex: 0, + constituentIndex: 0, + }, + { + perpMarketIndex: 1, + constituentIndex: 0, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammMapping = + await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + ); + expect(ammMapping).to.not.be.null; + assert(ammMapping.data.length == 2); + }); + + it('fails adding datum with bad params', async () => { + // Bad perp market index + try { + await adminClient.addInitAmmConstituentMappingData( + encodeName(lpPoolName), + [ + { + perpMarketIndex: 2, + constituentIndex: 0, + }, + ] + ); + expect.fail('should have failed'); + } catch (e) { + expect(e.message).to.contain('0x18ab'); + } + + // Bad constituent index + try { + await adminClient.addInitAmmConstituentMappingData( + encodeName(lpPoolName), + [ + { + perpMarketIndex: 0, + constituentIndex: 1, + }, + ] + ); + expect.fail('should have failed'); + } catch (e) { + expect(e.message).to.contain('0x18ab'); + } + }); });