From b04f6ec288ca6dd998e9b5c99aefa3045099be1d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 9 Oct 2024 17:14:38 -0400 Subject: [PATCH 01/11] init pool work --- programs/drift/src/instructions/admin.rs | 27 +++++++++++++++++-- programs/drift/src/instructions/user.rs | 33 ++++++++++++++++++++++-- programs/drift/src/lib.rs | 7 +++++ programs/drift/src/math/margin.rs | 18 +++++++++++++ programs/drift/src/state/perp_market.rs | 6 +++-- programs/drift/src/state/spot_market.rs | 6 +++-- programs/drift/src/state/user.rs | 3 ++- 7 files changed, 91 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index b8f9603dc..330d8086a 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -313,7 +313,8 @@ pub fn handle_initialize_spot_market( fuel_boost_maker: 0, fuel_boost_insurance: 0, token_program, - padding: [0; 41], + pool_id: 0, + padding: [0; 40], insurance_fund: InsuranceFund { vault: *ctx.accounts.insurance_fund_vault.to_account_info().key, unstaking_period: THIRTEEN_DAY, @@ -326,6 +327,27 @@ pub fn handle_initialize_spot_market( Ok(()) } +#[access_control( + spot_market_valid(&ctx.accounts.spot_market) +)] +pub fn handle_update_spot_market_pool_id( + ctx: Context, + pool_id: u8, +) -> Result<()> { + let mut spot_market = load_mut!(ctx.accounts.spot_market)?; + msg!("updating spot market {} expiry", spot_market.market_index); + + validate!( + spot_market.status == MarketStatus::Initialized, + ErrorCode::DefaultError, + "Market must be just initialized to update pool" + )?; + + spot_market.pool_id = pool_id; + + Ok(()) +} + pub fn handle_initialize_serum_fulfillment_config( ctx: Context, market_index: u16, @@ -855,7 +877,8 @@ pub fn handle_initialize_perp_market( fuel_boost_position: 0, fuel_boost_taker: 0, fuel_boost_maker: 0, - padding: [0; 43], + pool_id: 0, + padding: [0; 42], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index c7014ba69..46034b898 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -27,8 +27,9 @@ use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::{ - calculate_max_withdrawable_amount, meets_place_order_margin_requirement, - meets_withdraw_margin_requirement, validate_spot_margin_trading, MarginRequirementType, + calculate_max_withdrawable_amount, meets_initial_margin_requirement, + meets_place_order_margin_requirement, meets_withdraw_margin_requirement, + validate_spot_margin_trading, MarginRequirementType, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_value; @@ -1960,6 +1961,34 @@ pub fn handle_update_user_margin_trading_enabled<'c: 'info, 'info>( Ok(()) } +pub fn handle_update_user_pool_id<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, + _sub_account_id: u16, + pool_id: u8, +) -> Result<()> { + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + 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 mut user = load_mut!(ctx.accounts.user)?; + user.pool_id = pool_id; + + // will throw if user has deposits/positions in other pools + meets_initial_margin_requirement(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; + + Ok(()) +} + pub fn handle_update_user_delegate( ctx: Context, _sub_account_id: u16, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 3a96f4e17..fc56b675b 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -932,6 +932,13 @@ pub mod drift { handle_update_insurance_fund_unstaking_period(ctx, insurance_fund_unstaking_period) } + pub fn update_spot_market_pool_id( + ctx: Context, + pool_id: u8, + ) -> Result<()> { + handle_update_spot_market_pool_id(ctx, pool_id) + } + pub fn update_spot_market_liquidation_fee( ctx: Context, liquidator_fee: u32, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 0162f6b64..4cfa54823 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -245,6 +245,8 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0_u32 }; + let user_pool_id = user.pool_id; + for spot_position in user.spot_positions.iter() { validation::position::validate_spot_position(spot_position)?; @@ -261,6 +263,14 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.get_max_confidence_interval_multiplier()?, )?; + validate!( + user_pool_id == spot_market.pool_id, + ErrorCode::DefaultError, + "user pool id ({}) == spot market pool id ({})", + user_pool_id, + spot_market.pool_id, + )?; + calculation.update_all_oracles_valid(is_oracle_valid_for_action( oracle_validity, Some(DriftAction::MarginCalc), @@ -443,6 +453,14 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( let market = &perp_market_map.get_ref(&market_position.market_index)?; + validate!( + user_pool_id == market.pool_id, + ErrorCode::DefaultError, + "user pool id ({}) == perp market pool id ({})", + user_pool_id, + market.pool_id, + )?; + let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; let (quote_oracle_price_data, quote_oracle_validity) = oracle_map .get_price_data_and_validity( diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index d6e33c9d0..647d16df7 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -233,7 +233,8 @@ pub struct PerpMarket { /// fuel multiplier for perp maker /// precision: 10 pub fuel_boost_maker: u8, - pub padding: [u8; 43], + pub pool_id: u8, + pub padding: [u8; 42], } impl Default for PerpMarket { @@ -270,7 +271,8 @@ impl Default for PerpMarket { fuel_boost_position: 0, fuel_boost_taker: 0, fuel_boost_maker: 0, - padding: [0; 43], + pool_id: 0, + padding: [0; 42], } } } diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 2a0562a64..c5a1da9b7 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -201,7 +201,8 @@ pub struct SpotMarket { /// precision: 10 pub fuel_boost_insurance: u8, pub token_program: u8, - pub padding: [u8; 41], + pub pool_id: u8, + pub padding: [u8; 40], } impl Default for SpotMarket { @@ -269,7 +270,8 @@ impl Default for SpotMarket { fuel_boost_maker: 0, fuel_boost_insurance: 0, token_program: 0, - padding: [0; 41], + pool_id: 0, + padding: [0; 40], } } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index ce34bbc8d..9326860ba 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -126,7 +126,8 @@ pub struct User { pub open_auctions: u8, /// Whether or not user has open order with auction pub has_open_auction: bool, - pub padding1: [u8; 5], + pub pool_id: u8, + pub padding1: [u8; 4], pub last_fuel_bonus_update_ts: u32, pub padding: [u8; 12], } From 210ef9454d4e4ac29bbdaf7ac61cce7d2e9d339d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 9 Oct 2024 17:36:13 -0400 Subject: [PATCH 02/11] add some invariants --- programs/drift/src/controller/orders.rs | 14 ++++++++++++++ programs/drift/src/error.rs | 2 ++ programs/drift/src/instructions/user.rs | 24 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 486e0d69e..3894db26e 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -168,6 +168,13 @@ pub fn place_perp_order( "Market is being initialized" )?; + validate!( + user.pool_id == 0, + ErrorCode::InvalidPoolId, + "user pool id ({}) != 0", + user.pool_id + )?; + validate!( !market.is_in_settlement(now), ErrorCode::MarketPlaceOrderPaused, @@ -3287,6 +3294,13 @@ pub fn place_spot_order( let force_reduce_only = spot_market.is_reduce_only(); let step_size = spot_market.order_step_size; + validate!( + user.pool_id == 0, + ErrorCode::InvalidPoolId, + "user pool id ({}) != 0", + user.pool_id + )?; + validate!( !matches!(spot_market.status, MarketStatus::Initialized), ErrorCode::MarketBeingInitialized, diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 5672a3ff4..f3362da79 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -585,6 +585,8 @@ pub enum ErrorCode { InvalidSwiftOrderParam, #[msg("Place and take order success condition failed")] PlaceAndTakeOrderSuccessConditionFailed, + #[msg("Invalid pool id")] + InvalidPoolId, } #[macro_export] diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 46034b898..cb3d86f33 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -301,6 +301,14 @@ pub fn handle_deposit<'c: 'info, 'info>( let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; let oracle_price_data = &oracle_map.get_price_data(&spot_market.oracle)?.clone(); + validate!( + user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + user.pool_id, + spot_market.pool_id + )?; + validate!( !matches!(spot_market.status, MarketStatus::Initialized), ErrorCode::MarketBeingInitialized, @@ -680,6 +688,14 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; + validate!( + from_user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + from_user.pool_id, + spot_market.pool_id + )?; + from_user.increment_total_withdraws( amount, oracle_price, @@ -747,6 +763,14 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; + validate!( + to_user.pool_id == spot_market.pool_id, + ErrorCode::InvalidPoolId, + "user pool id ({}) != market pool id ({})", + to_user.pool_id, + spot_market.pool_id + )?; + to_user.increment_total_deposits( amount, oracle_price, From 74df81431d80938fda4010c2ad9de4f92554f57d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 9 Oct 2024 18:05:10 -0400 Subject: [PATCH 03/11] add more invariants --- programs/drift/src/controller/liquidation.rs | 44 ++++++++++++++++++++ programs/drift/src/controller/orders.rs | 16 +++++++ 2 files changed, 60 insertions(+) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 955d50d70..e0df8ff25 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -107,6 +107,13 @@ pub fn liquidate_perp( "liquidator bankrupt", )?; + validate!( + liquidator.pool_id == 0, + ErrorCode::InvalidPoolId, + "liquidator pool id ({}) != 0", + liquidator.pool_id + )?; + let market = perp_market_map.get_ref(&market_index)?; validate!( @@ -689,6 +696,13 @@ pub fn liquidate_perp_with_fill( "user bankrupt", )?; + validate!( + liquidator.pool_id == 0, + ErrorCode::InvalidPoolId, + "liquidator pool id ({}) != 0", + liquidator.pool_id + )?; + validate!( !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, @@ -1153,6 +1167,14 @@ pub fn liquidate_spot( asset_market_index )?; + validate!( + liquidator.pool_id == asset_spot_market.pool_id, + ErrorCode::InvalidPoolId, + "liquidator pool id ({}) != asset spot market pool id ({})", + liquidator.pool_id, + asset_spot_market.pool_id + )?; + drop(asset_spot_market); let liability_spot_market = spot_market_map.get_ref(&liability_market_index)?; @@ -1164,6 +1186,14 @@ pub fn liquidate_spot( liability_market_index )?; + validate!( + liquidator.pool_id == liability_spot_market.pool_id, + ErrorCode::InvalidPoolId, + "liquidator pool id ({}) != liablity spot market pool id ({})", + liquidator.pool_id, + liability_spot_market.pool_id + )?; + drop(liability_spot_market); // validate user and liquidator have spot balances @@ -1685,6 +1715,13 @@ pub fn liquidate_borrow_for_perp_pnl( "liquidator bankrupt", )?; + validate!( + liquidator.pool_id == 0, + ErrorCode::InvalidPoolId, + "liquidator pool id ({}) != 0", + liquidator.pool_id + )?; + let perp_market = perp_market_map.get_ref(&perp_market_index)?; validate!( @@ -2154,6 +2191,13 @@ pub fn liquidate_perp_pnl_for_deposit( "liquidator bankrupt", )?; + validate!( + liquidator.pool_id == 0, + ErrorCode::InvalidPoolId, + "liquidator pool id ({}) != 0", + liquidator.pool_id + )?; + let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; validate!( diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 3894db26e..70cb51c87 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1031,6 +1031,14 @@ pub fn fill_perp_order( let is_filler_maker = makers_and_referrer.0.contains_key(&filler_key); let (mut filler, mut filler_stats) = if !is_filler_maker && !is_filler_taker { let filler = load_mut!(filler)?; + + validate!( + filler.pool_id == 0, + ErrorCode::InvalidPoolId, + "filler pool id ({}) != 0", + filler.pool_id + )?; + if filler.authority != user.authority { (Some(filler), Some(load_mut!(filler_stats)?)) } else { @@ -3582,6 +3590,14 @@ pub fn fill_spot_order( let is_filler_maker = makers_and_referrer.0.contains_key(&filler_key); let (mut filler, mut filler_stats) = if !is_filler_maker && !is_filler_taker { let filler = load_mut!(filler)?; + + validate!( + filler.pool_id == 0, + ErrorCode::InvalidPoolId, + "filler pool id ({}) != 0", + filler.pool_id + )?; + if filler.authority != user.authority { (Some(filler), Some(load_mut!(filler_stats)?)) } else { From e73f4dbb69c6544883d96737200e65d9205875db Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 24 Oct 2024 08:29:12 -0400 Subject: [PATCH 04/11] start adding test --- programs/drift/src/instructions/admin.rs | 6 - programs/drift/src/validation/margin.rs | 8 +- sdk/src/adminClient.ts | 34 + sdk/src/idl/drift.json | 49 +- sdk/src/types.ts | 2 + test-scripts/run-anchor-tests.sh | 1 + tests/spotMarketPoolIds.ts | 802 +++++++++++++++++++++++ 7 files changed, 889 insertions(+), 13 deletions(-) create mode 100644 tests/spotMarketPoolIds.ts diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 330d8086a..daa471292 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -156,12 +156,6 @@ pub fn handle_initialize_spot_market( ErrorCode::InvalidSpotMarketInitialization, "For OracleSource::QuoteAsset, oracle must be default public key" )?; - - validate!( - spot_market_index == QUOTE_SPOT_MARKET_INDEX, - ErrorCode::InvalidSpotMarketInitialization, - "For OracleSource::QuoteAsset, spot_market_index must be QUOTE_SPOT_MARKET_INDEX" - )?; } else { OracleMap::validate_oracle_account_info(&ctx.accounts.oracle)?; } diff --git a/programs/drift/src/validation/margin.rs b/programs/drift/src/validation/margin.rs index a818c4360..d264f8eb3 100644 --- a/programs/drift/src/validation/margin.rs +++ b/programs/drift/src/validation/margin.rs @@ -79,7 +79,7 @@ pub fn validate_margin_weights( )?; } else { validate!( - initial_asset_weight < SPOT_WEIGHT_PRECISION, + initial_asset_weight <= SPOT_WEIGHT_PRECISION, ErrorCode::InvalidSpotMarketInitialization, "Initial asset weight must be less than {}", SPOT_WEIGHT_PRECISION @@ -88,14 +88,14 @@ pub fn validate_margin_weights( validate!( initial_asset_weight <= maintenance_asset_weight && maintenance_asset_weight > 0 - && maintenance_asset_weight < SPOT_WEIGHT_PRECISION, + && maintenance_asset_weight <= SPOT_WEIGHT_PRECISION, ErrorCode::InvalidSpotMarketInitialization, "Maintenance asset weight must be between 0 {}", SPOT_WEIGHT_PRECISION )?; validate!( - initial_liability_weight > SPOT_WEIGHT_PRECISION, + initial_liability_weight >= SPOT_WEIGHT_PRECISION, ErrorCode::InvalidSpotMarketInitialization, "Initial liability weight must be greater than {}", SPOT_WEIGHT_PRECISION @@ -103,7 +103,7 @@ pub fn validate_margin_weights( validate!( initial_liability_weight >= maintenance_liability_weight - && maintenance_liability_weight > SPOT_WEIGHT_PRECISION, + && maintenance_liability_weight >= SPOT_WEIGHT_PRECISION, ErrorCode::InvalidSpotMarketInitialization, "Maintenance liability weight must be greater than {}", SPOT_WEIGHT_PRECISION diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 709012874..6347b9eb7 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1452,6 +1452,40 @@ export class AdminClient extends DriftClient { }); } + public async updateSpotMarketPoolId( + spotMarketIndex: number, + poolId: number + ): Promise { + const updateSpotMarketPoolIdIx = await this.getUpdateSpotMarketPoolIdIx( + spotMarketIndex, + poolId + ); + + const tx = await this.buildTransaction(updateSpotMarketPoolIdIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateSpotMarketPoolIdIx( + spotMarketIndex: number, + poolId: number + ): Promise { + return await this.program.instruction.updateSpotMarketPoolId(poolId, { + accounts: { + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: await getSpotMarketPublicKey( + this.program.programId, + spotMarketIndex + ), + }, + }); + } + public async updatePerpMarketPerLpBase( perpMarketIndex: number, perLpBase: number diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index da88c7f25..8cb5b7582 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -4103,6 +4103,32 @@ } ] }, + { + "name": "updateSpotMarketPoolId", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "poolId", + "type": "u8" + } + ] + }, { "name": "updateSpotMarketLiquidationFee", "accounts": [ @@ -6546,12 +6572,16 @@ ], "type": "u8" }, + { + "name": "poolId", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 43 + 42 ] } } @@ -7048,12 +7078,16 @@ "name": "tokenProgram", "type": "u8" }, + { + "name": "poolId", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 41 + 40 ] } } @@ -7401,12 +7435,16 @@ ], "type": "bool" }, + { + "name": "poolId", + "type": "u8" + }, { "name": "padding1", "type": { "array": [ "u8", - 5 + 4 ] } }, @@ -13152,6 +13190,11 @@ "code": 6289, "name": "PlaceAndTakeOrderSuccessConditionFailed", "msg": "Place and take order success condition failed" + }, + { + "code": 6290, + "name": "InvalidPoolId", + "msg": "Invalid pool id" } ], "metadata": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 47d5731e4..5ddce3b80 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -767,6 +767,8 @@ export type SpotMarketAccount = { fuelBoostInsurance: number; tokenProgram: number; + + poolId: number; }; export type PoolBalance = { diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 879e70bdc..394274d52 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -64,6 +64,7 @@ test_files=( serumTest.ts spotDepositWithdraw.ts spotDepositWithdraw22.ts + spotMarketPoolIds.ts spotSwap.ts spotSwap22.ts stopLimits.ts diff --git a/tests/spotMarketPoolIds.ts b/tests/spotMarketPoolIds.ts new file mode 100644 index 000000000..8a74a0047 --- /dev/null +++ b/tests/spotMarketPoolIds.ts @@ -0,0 +1,802 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; + +import { + TestClient, + BN, + EventSubscriber, + SPOT_MARKET_RATE_PRECISION, + SpotBalanceType, + isVariant, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION, + OracleInfo, + MarketStatus, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + createUserWithUSDCAndWSOLAccount, + mintUSDCToUser, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + sleep, +} from './testHelpers'; +import { + getBalance, + calculateInterestAccumulated, + getTokenAmount, +} from '../sdk/src/math/spotBalance'; +import { NATIVE_MINT } from '@solana/spl-token'; +import { + QUOTE_PRECISION, + ZERO, + ONE, + SPOT_MARKET_BALANCE_PRECISION, + PRICE_PRECISION, +} from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('spot deposit and withdraw', () => { + const chProgram = anchor.workspace.Drift as Program; + + let admin: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let solOracle: PublicKey; + + let usdcMint; + + let firstUserDriftClient: TestClient; + let firstUserDriftClientUSDCAccount: PublicKey; + + let secondUserDriftClient: TestClient; + let secondUserDriftClientWSOLAccount: PublicKey; + let secondUserDriftClientUSDCAccount: PublicKey; + + const usdcAmount = new BN(10 * 10 ** 6); + const largeUsdcAmount = new BN(10_000 * 10 ** 6); + + const solAmount = new BN(1 * 10 ** 9); + + let marketIndexes: number[]; + let spotMarketIndexes: number[]; + let oracleInfos: OracleInfo[]; + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + await mockUserUSDCAccount(usdcMint, largeUsdcAmount, bankrunContextWrapper); + + solOracle = await mockOracleNoProgram(bankrunContextWrapper, 30); + + marketIndexes = []; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solOracle, source: OracleSource.PYTH }]; + + admin = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await admin.initialize(usdcMint.publicKey, true); + await admin.subscribe(); + }); + + after(async () => { + await admin.unsubscribe(); + await eventSubscriber.unsubscribe(); + await firstUserDriftClient.unsubscribe(); + await secondUserDriftClient.unsubscribe(); + }); + + it('Initialize Markets', async () => { + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(20)).toNumber(); // 2000% APR + const maxRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(50)).toNumber(); // 5000% APR + 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(); + await admin.initializeSpotMarket( + usdcMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + PublicKey.default, + OracleSource.QUOTE_ASSET, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight + ); + await admin.updateWithdrawGuardThreshold( + 0, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + await admin.fetchAccounts(); + const spotMarket = await admin.getSpotMarketAccount(0); + assert(spotMarket.poolId === 0); + + await admin.initializeSpotMarket( + usdcMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + PublicKey.default, + OracleSource.QUOTE_ASSET, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + undefined, + undefined, + undefined, + false, + ); + await admin.updateSpotMarketPoolId(1, 1); + await admin.updateSpotMarketStatus(1, MarketStatus.ACTIVE); + await admin.updateWithdrawGuardThreshold( + 0, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + await admin.fetchAccounts(); + await admin.fetchAccounts(); + const spotMarket1 = await admin.getSpotMarketAccount(1); + assert(spotMarket1.poolId === 1); + }); + + it('First User Deposit USDC', async () => { + [firstUserDriftClient, firstUserDriftClientUSDCAccount] = + await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + await sleep(100); + await firstUserDriftClient.fetchAccounts(); + await firstUserDriftClient.deposit( + usdcAmount.divn(2), + 0, + firstUserDriftClientUSDCAccount + ); + + try { + await firstUserDriftClient.deposit( + usdcAmount.divn(2), + 1, + firstUserDriftClientUSDCAccount + ); + assert(false); + } catch (e) { + assert(true); + } + }); + + it('Second User Deposit USDC', async () => { + [secondUserDriftClient, secondUserDriftClientUSDCAccount] = + await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + await sleep(100); + await secondUserDriftClient.fetchAccounts(); + await secondUserDriftClient.deposit( + usdcAmount.divn(2), + 1, + secondUserDriftClientUSDCAccount + ); + + try { + await secondUserDriftClient.deposit( + usdcAmount.divn(2), + 0, + secondUserDriftClientUSDCAccount + ); + assert(false); + } catch (e) { + assert(true); + } + }); + + // it('Second User Deposit SOL', async () => { + // [ + // secondUserDriftClient, + // secondUserDriftClientWSOLAccount, + // secondUserDriftClientUSDCAccount, + // ] = await createUserWithUSDCAndWSOLAccount( + // bankrunContextWrapper, + // usdcMint, + // chProgram, + // solAmount, + // ZERO, + // marketIndexes, + // spotMarketIndexes, + // oracleInfos, + // bulkAccountLoader + // ); + + // const marketIndex = 1; + // const txSig = await secondUserDriftClient.deposit( + // solAmount, + // marketIndex, + // secondUserDriftClientWSOLAccount + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // const spotMarket = await admin.getSpotMarketAccount(marketIndex); + // assert(spotMarket.depositBalance.eq(SPOT_MARKET_BALANCE_PRECISION)); + // console.log(spotMarket.historicalOracleData); + // assert(spotMarket.historicalOracleData.lastOraclePriceTwapTs.gt(ZERO)); + // assert( + // spotMarket.historicalOracleData.lastOraclePrice.eq( + // new BN(30 * PRICE_PRECISION.toNumber()) + // ) + // ); + // assert( + // spotMarket.historicalOracleData.lastOraclePriceTwap.eq( + // new BN(30 * PRICE_PRECISION.toNumber()) + // ) + // ); + // assert( + // spotMarket.historicalOracleData.lastOraclePriceTwap5Min.eq( + // new BN(30 * PRICE_PRECISION.toNumber()) + // ) + // ); + + // const vaultAmount = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount(spotMarket.vault) + // ).amount.toString() + // ); + // assert(vaultAmount.eq(solAmount)); + + // const expectedBalance = getBalance( + // solAmount, + // spotMarket, + // SpotBalanceType.DEPOSIT + // ); + // const spotPosition = + // secondUserDriftClient.getUserAccount().spotPositions[1]; + // assert(isVariant(spotPosition.balanceType, 'deposit')); + // assert(spotPosition.scaledBalance.eq(expectedBalance)); + + // assert( + // secondUserDriftClient + // .getUserAccount() + // .totalDeposits.eq(new BN(30).mul(PRICE_PRECISION)) + // ); + // }); + + // it('Second User Withdraw First half USDC', async () => { + // const marketIndex = 0; + // const withdrawAmount = usdcAmount.div(new BN(2)); + // const txSig = await secondUserDriftClient.withdraw( + // withdrawAmount, + // marketIndex, + // secondUserDriftClientUSDCAccount + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // const spotMarket = await admin.getSpotMarketAccount(marketIndex); + // const expectedBorrowBalance = new BN(5000000001); + // assert(spotMarket.borrowBalance.eq(expectedBorrowBalance)); + + // const vaultAmount = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount(spotMarket.vault) + // ).amount.toString() + // ); + // const expectedVaultAmount = usdcAmount.sub(withdrawAmount); + // assert(vaultAmount.eq(expectedVaultAmount)); + + // const expectedBalance = getBalance( + // withdrawAmount, + // spotMarket, + // SpotBalanceType.BORROW + // ); + + // const spotPosition = + // secondUserDriftClient.getUserAccount().spotPositions[0]; + // assert(isVariant(spotPosition.balanceType, 'borrow')); + // assert(spotPosition.scaledBalance.eq(expectedBalance)); + + // const actualAmountWithdrawn = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientUSDCAccount + // ) + // ).amount.toString() + // ); + + // assert(withdrawAmount.eq(actualAmountWithdrawn)); + + // assert( + // secondUserDriftClient.getUserAccount().totalWithdraws.eq(withdrawAmount) + // ); + // }); + + // it('Update Cumulative Interest with 50% utilization', async () => { + // const usdcmarketIndex = 0; + // const oldSpotMarketAccount = + // firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + // await sleep(5000); + + // const txSig = await firstUserDriftClient.updateSpotMarketCumulativeInterest( + // usdcmarketIndex + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // await firstUserDriftClient.fetchAccounts(); + // const newSpotMarketAccount = + // firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + // const expectedInterestAccumulated = calculateInterestAccumulated( + // oldSpotMarketAccount, + // newSpotMarketAccount.lastInterestTs + // ); + // const expectedCumulativeDepositInterest = + // oldSpotMarketAccount.cumulativeDepositInterest.add( + // expectedInterestAccumulated.depositInterest + // ); + // const expectedCumulativeBorrowInterest = + // oldSpotMarketAccount.cumulativeBorrowInterest.add( + // expectedInterestAccumulated.borrowInterest + // ); + + // assert( + // newSpotMarketAccount.cumulativeDepositInterest.eq( + // expectedCumulativeDepositInterest + // ) + // ); + // assert( + // newSpotMarketAccount.cumulativeBorrowInterest.eq( + // expectedCumulativeBorrowInterest + // ) + // ); + // }); + + // it('Second User Withdraw second half USDC', async () => { + // const marketIndex = 0; + // let spotMarketAccount = + // secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const spotMarketDepositTokenAmountBefore = getTokenAmount( + // spotMarketAccount.depositBalance, + // spotMarketAccount, + // SpotBalanceType.DEPOSIT + // ); + // const spotMarketBorrowTokenAmountBefore = getTokenAmount( + // spotMarketAccount.borrowBalance, + // spotMarketAccount, + // SpotBalanceType.BORROW + // ); + // const spotMarketBorrowBalanceBefore = spotMarketAccount.borrowBalance; + + // const userUSDCAmountBefore = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientUSDCAccount + // ) + // ).amount.toString() + // ); + + // const spotPositionBefore = + // secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + + // const withdrawAmount = spotMarketDepositTokenAmountBefore + // .sub(spotMarketBorrowTokenAmountBefore) + // .sub(ONE); + + // const txSig = await secondUserDriftClient.withdraw( + // withdrawAmount, + // marketIndex, + // secondUserDriftClientUSDCAccount + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // spotMarketAccount = secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const increaseInspotPosition = getBalance( + // withdrawAmount, + // spotMarketAccount, + // SpotBalanceType.BORROW + // ); + // const expectedspotPosition = spotPositionBefore.add(increaseInspotPosition); + // console.log('withdrawAmount:', withdrawAmount.toString()); + + // assert( + // secondUserDriftClient + // .getSpotPosition(marketIndex) + // .scaledBalance.eq(expectedspotPosition) + // ); + + // const expectedUserUSDCAmount = userUSDCAmountBefore.add(withdrawAmount); + // const userUSDCAmountAfter = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientUSDCAccount + // ) + // ).amount.toString() + // ); + + // assert(expectedUserUSDCAmount.eq(userUSDCAmountAfter)); + // assert( + // secondUserDriftClient + // .getUserAccount() + // .totalWithdraws.eq(userUSDCAmountAfter) + // ); + + // const expectedSpotMarketBorrowBalance = spotMarketBorrowBalanceBefore.add( + // increaseInspotPosition + // ); + // console.assert( + // spotMarketAccount.borrowBalance.eq(expectedSpotMarketBorrowBalance) + // ); + + // const expectedVaultBalance = usdcAmount.sub(expectedUserUSDCAmount); + // const vaultUSDCAmountAfter = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // spotMarketAccount.vault + // ) + // ).amount.toString() + // ); + + // assert(expectedVaultBalance.eq(vaultUSDCAmountAfter)); + + // const spotMarketDepositTokenAmountAfter = getTokenAmount( + // spotMarketAccount.depositBalance, + // spotMarketAccount, + // SpotBalanceType.DEPOSIT + // ); + // const spotMarketBorrowTokenAmountAfter = getTokenAmount( + // spotMarketAccount.borrowBalance, + // spotMarketAccount, + // SpotBalanceType.BORROW + // ); + + // // TODO + // console.log( + // spotMarketDepositTokenAmountAfter.toString(), + // spotMarketBorrowTokenAmountAfter.toString() + // ); + // assert( + // spotMarketDepositTokenAmountAfter + // .sub(spotMarketBorrowTokenAmountAfter) + // .lte(ONE) + // ); + // }); + + // it('Update Cumulative Interest with 100% utilization', async () => { + // const usdcmarketIndex = 0; + // const oldSpotMarketAccount = + // firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + // await sleep(5000); + + // const txSig = await firstUserDriftClient.updateSpotMarketCumulativeInterest( + // usdcmarketIndex + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // await firstUserDriftClient.fetchAccounts(); + // const newSpotMarketAccount = + // firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + // const expectedInterestAccumulated = calculateInterestAccumulated( + // oldSpotMarketAccount, + // newSpotMarketAccount.lastInterestTs + // ); + // const expectedCumulativeDepositInterest = + // oldSpotMarketAccount.cumulativeDepositInterest.add( + // expectedInterestAccumulated.depositInterest + // ); + // const expectedCumulativeBorrowInterest = + // oldSpotMarketAccount.cumulativeBorrowInterest.add( + // expectedInterestAccumulated.borrowInterest + // ); + + // assert( + // newSpotMarketAccount.cumulativeDepositInterest.eq( + // expectedCumulativeDepositInterest + // ) + // ); + // console.log( + // newSpotMarketAccount.cumulativeBorrowInterest.sub(ONE).toString(), + // expectedCumulativeBorrowInterest.toString() + // ); + + // // inconcistent time leads to slight differences over runs? + // assert( + // newSpotMarketAccount.cumulativeBorrowInterest + // .sub(ONE) + // .eq(expectedCumulativeBorrowInterest) || + // newSpotMarketAccount.cumulativeBorrowInterest.eq( + // expectedCumulativeBorrowInterest + // ) + // ); + // }); + + // it('Flip second user borrow to deposit', async () => { + // const marketIndex = 0; + // const mintAmount = new BN(2 * 10 ** 6); // $2 + // const userUSDCAmountBefore = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientUSDCAccount + // ) + // ).amount.toString() + // ); + + // await mintUSDCToUser( + // usdcMint, + // secondUserDriftClientUSDCAccount, + // mintAmount, + // bankrunContextWrapper + // ); + + // const userBorrowBalanceBefore = + // secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + // const spotMarketDepositBalanceBefore = + // secondUserDriftClient.getSpotMarketAccount(marketIndex).depositBalance; + + // const depositAmount = userUSDCAmountBefore.add(mintAmount.div(new BN(2))); + // const txSig = await secondUserDriftClient.deposit( + // depositAmount, + // marketIndex, + // secondUserDriftClientUSDCAccount + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // await secondUserDriftClient.fetchAccounts(); + // const spotMarketAccount = + // secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const borrowToPayOff = getTokenAmount( + // userBorrowBalanceBefore, + // spotMarketAccount, + // SpotBalanceType.BORROW + // ); + // const newDepositTokenAmount = depositAmount.sub(borrowToPayOff); + + // const expectedUserBalance = getBalance( + // newDepositTokenAmount, + // spotMarketAccount, + // SpotBalanceType.DEPOSIT + // ); + // const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + + // console.log( + // expectedUserBalance.toString(), + // userBalanceAfter.scaledBalance.toString() + // ); + + // assert(expectedUserBalance.eq(userBalanceAfter.scaledBalance)); + // assert(isVariant(userBalanceAfter.balanceType, 'deposit')); + + // const expectedSpotMarketDepositBalance = + // spotMarketDepositBalanceBefore.add(expectedUserBalance); + + // console.log( + // spotMarketAccount.depositBalance.toString(), + // expectedSpotMarketDepositBalance.toString() + // ); + + // assert( + // spotMarketAccount.depositBalance.eq(expectedSpotMarketDepositBalance) + // ); + // assert(spotMarketAccount.borrowBalance.eq(ZERO)); + // }); + + // it('Flip second user deposit to borrow', async () => { + // const marketIndex = 0; + + // const spotMarketAccountBefore = + // secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const userDepositBalanceBefore = + // secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + // const spotMarketDepositBalanceBefore = + // secondUserDriftClient.getSpotMarketAccount(marketIndex).depositBalance; + // const userDepositokenAmountBefore = getTokenAmount( + // userDepositBalanceBefore, + // spotMarketAccountBefore, + // SpotBalanceType.DEPOSIT + // ); + + // const borrowAmount = userDepositokenAmountBefore.add(new BN(1 * 10 ** 6)); + // const txSig = await secondUserDriftClient.withdraw( + // borrowAmount, + // marketIndex, + // secondUserDriftClientUSDCAccount + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // await secondUserDriftClient.fetchAccounts(); + // const spotMarketAccount = + // secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const depositToWithdrawAgainst = getTokenAmount( + // userDepositBalanceBefore, + // spotMarketAccount, + // SpotBalanceType.DEPOSIT + // ); + // const newBorrowTokenAmount = borrowAmount.sub(depositToWithdrawAgainst); + + // const expectedUserBalance = getBalance( + // newBorrowTokenAmount, + // spotMarketAccount, + // SpotBalanceType.BORROW + // ); + // const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + + // assert(expectedUserBalance.eq(userBalanceAfter.scaledBalance)); + // assert(isVariant(userBalanceAfter.balanceType, 'borrow')); + + // const expectedSpotMarketDepositBalance = spotMarketDepositBalanceBefore.sub( + // userDepositBalanceBefore + // ); + // assert( + // spotMarketAccount.depositBalance.eq(expectedSpotMarketDepositBalance) + // ); + // assert(spotMarketAccount.borrowBalance.eq(expectedUserBalance)); + // }); + + // it('Second user reduce only pay down borrow', async () => { + // const marketIndex = 0; + // const userUSDCAmountBefore = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientUSDCAccount + // ) + // ).amount.toString() + // ); + + // const currentUserBorrowBalance = + // secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + // const spotMarketDepositBalanceBefore = + // secondUserDriftClient.getSpotMarketAccount(marketIndex).depositBalance; + + // const depositAmount = userUSDCAmountBefore.mul(new BN(100000)); // huge number + // const txSig = await secondUserDriftClient.deposit( + // depositAmount, + // marketIndex, + // secondUserDriftClientUSDCAccount, + // undefined, + // true + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // const spotMarketAccountAfter = + // secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const borrowToPayBack = getTokenAmount( + // currentUserBorrowBalance, + // spotMarketAccountAfter, + // SpotBalanceType.BORROW + // ); + + // const userUSDCAmountAfter = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientUSDCAccount + // ) + // ).amount.toString() + // ); + + // const expectedUserUSDCAmount = userUSDCAmountBefore.sub(borrowToPayBack); + // console.log( + // expectedUserUSDCAmount.toString(), + // userUSDCAmountAfter.toString() + // ); + // assert(expectedUserUSDCAmount.eq(userUSDCAmountAfter)); + + // const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + // assert(userBalanceAfter.scaledBalance.eq(ZERO)); + + // assert(spotMarketAccountAfter.borrowBalance.eq(ZERO)); + // assert( + // spotMarketAccountAfter.depositBalance.eq(spotMarketDepositBalanceBefore) + // ); + // }); + + // it('Second user reduce only withdraw deposit', async () => { + // const marketIndex = 1; + // const userWSOLAmountBefore = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientWSOLAccount + // ) + // ).amount.toString() + // ); + + // const currentUserDepositBalance = + // secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + + // const withdrawAmount = new BN(LAMPORTS_PER_SOL * 100); + // const txSig = await secondUserDriftClient.withdraw( + // withdrawAmount, + // marketIndex, + // secondUserDriftClientWSOLAccount, + // true + // ); + // bankrunContextWrapper.printTxLogs(txSig); + + // const spotMarketAccountAfter = + // secondUserDriftClient.getSpotMarketAccount(marketIndex); + // const amountAbleToWithdraw = getTokenAmount( + // currentUserDepositBalance, + // spotMarketAccountAfter, + // SpotBalanceType.DEPOSIT + // ); + + // const userWSOLAmountAfter = new BN( + // ( + // await bankrunContextWrapper.connection.getTokenAccount( + // secondUserDriftClientWSOLAccount + // ) + // ).amount.toString() + // ); + + // const expectedUserWSOLAmount = + // amountAbleToWithdraw.sub(userWSOLAmountBefore); + // console.log(expectedUserWSOLAmount.toString()); + // console.log(userWSOLAmountAfter.toString()); + // assert(expectedUserWSOLAmount.eq(userWSOLAmountAfter)); + + // const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + // assert(userBalanceAfter.scaledBalance.eq(ZERO)); + // }); +}); From 5eabe03eb961dd361adae88dce3c3806aa0ac296 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 24 Oct 2024 09:08:29 -0400 Subject: [PATCH 05/11] add ts to update user pool id --- package.json | 3 ++- programs/drift/src/lib.rs | 8 +++++++ sdk/src/driftClient.ts | 37 ++++++++++++++++++++++++++++++ sdk/src/idl/drift.json | 25 ++++++++++++++++++++ test-scripts/single-anchor-test.sh | 2 +- tests/spotMarketPoolIds.ts | 1 + 6 files changed, 74 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7f75e7421..103261f73 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "prettify": "prettier --check './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", "prettify:fix": "prettier --write './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", "lint": "eslint . --ext ts --quiet", - "lint:fix": "eslint . --ext ts --fix" + "lint:fix": "eslint . --ext ts --fix", + "update-idl": "cp target/idl/drift.json sdk/src/idl/drift.json" }, "engines": { "node": ">=12" diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index b2ce29823..03f7688ea 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -303,6 +303,14 @@ pub mod drift { handle_update_user_margin_trading_enabled(ctx, _sub_account_id, margin_trading_enabled) } + pub fn update_user_pool_id<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, + _sub_account_id: u16, + pool_id: u8, + ) -> Result<()> { + handle_update_user_pool_id(ctx, _sub_account_id, pool_id) + } + pub fn update_user_delegate( ctx: Context, _sub_account_id: u16, diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 98f7d0a90..76100c4df 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1353,6 +1353,43 @@ export class DriftClient { return ix; } + public async updateUserPoolId( + updates: { poolId: number; subAccountId: number }[] + ): Promise { + const ixs = await Promise.all( + updates.map(async ({ poolId, subAccountId }) => { + return await this.getUpdateUserPoolIdIx(poolId, subAccountId); + }) + ); + + const tx = await this.buildTransaction(ixs, this.txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdateUserPoolIdIx( + poolId: number, + subAccountId: number + ) { + const ix = await this.program.instruction.updateUserPoolId( + subAccountId, + poolId, + { + accounts: { + user: getUserAccountPublicKeySync( + this.program.programId, + this.wallet.publicKey, + subAccountId + ), + authority: this.wallet.publicKey, + }, + } + ); + + return ix; + } + public async fetchAllUserAccounts( includeIdle = true ): Promise[]> { diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index ec427eb5d..c0e1fa779 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1232,6 +1232,31 @@ } ] }, + { + "name": "updateUserPoolId", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "poolId", + "type": "u8" + } + ] + }, { "name": "updateUserDelegate", "accounts": [ diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 9c41d814e..8d5705e30 100644 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,7 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(referrer.ts) +test_files=(spotMarketPoolIds.ts) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/spotMarketPoolIds.ts b/tests/spotMarketPoolIds.ts index 8a74a0047..f74934f49 100644 --- a/tests/spotMarketPoolIds.ts +++ b/tests/spotMarketPoolIds.ts @@ -235,6 +235,7 @@ describe('spot deposit and withdraw', () => { bulkAccountLoader ); + await secondUserDriftClient.updateUserPoolId([{subAccountId: 0, poolId: 1}]); await sleep(100); await secondUserDriftClient.fetchAccounts(); await secondUserDriftClient.deposit( From 7c04dd5ed3f5bc2b18426545e9bba65d0c982d5e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Fri, 25 Oct 2024 08:20:51 -0400 Subject: [PATCH 06/11] add tests for margin --- programs/drift/src/instructions/user.rs | 4 +- programs/drift/src/math/margin.rs | 4 +- programs/drift/src/math/margin/tests.rs | 141 ++++++++++++++++++++ programs/drift/src/state/spot_market_map.rs | 4 + 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a8982eee8..bd5d7a999 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -30,8 +30,8 @@ use crate::math::casting::Cast; use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::{ calculate_max_withdrawable_amount, meets_initial_margin_requirement, - meets_maintenance_margin_requirement, meets_place_order_margin_requirement, meets_withdraw_margin_requirement, - validate_spot_margin_trading, MarginRequirementType, + meets_maintenance_margin_requirement, meets_place_order_margin_requirement, + meets_withdraw_margin_requirement, validate_spot_margin_trading, MarginRequirementType, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_value; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 907fb2329..1bdc0ae0b 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -268,7 +268,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( validate!( user_pool_id == spot_market.pool_id, - ErrorCode::DefaultError, + ErrorCode::InvalidPoolId, "user pool id ({}) == spot market pool id ({})", user_pool_id, spot_market.pool_id, @@ -458,7 +458,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( validate!( user_pool_id == market.pool_id, - ErrorCode::DefaultError, + ErrorCode::InvalidPoolId, "user pool id ({}) == perp market pool id ({})", user_pool_id, market.pool_id, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 50cd0c798..78ae01f3d 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -3675,3 +3675,144 @@ mod calculate_perp_position_value_and_pnl_prediction_market { assert_eq!(upnl, 500000); //0 } } + +#[cfg(test)] +mod pools { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::create_anchor_account_info; + use crate::error::ErrorCode; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, MARGIN_PRECISION, + PEG_PRECISION, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::state::margin_calculation::{MarginCalculation, MarginContext}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::{get_positions, get_pyth_price}; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn spot() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let market_map = PerpMarketMap::empty(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + pool_id: 1, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + spot_positions, + ..User::default() + }; + + let result = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ); + + assert_eq!(result.unwrap_err(), ErrorCode::InvalidPoolId) + } + + #[test] + fn perp_market_invalid_pool_id() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + oracle: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + pool_id: 1, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let spot_market_map = SpotMarketMap::empty(); + + let user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 100 * BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let result = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ); + + assert_eq!(result.unwrap_err(), ErrorCode::InvalidPoolId) + } +} diff --git a/programs/drift/src/state/spot_market_map.rs b/programs/drift/src/state/spot_market_map.rs index 57290330e..6aa5c5474 100644 --- a/programs/drift/src/state/spot_market_map.rs +++ b/programs/drift/src/state/spot_market_map.rs @@ -271,6 +271,10 @@ impl<'a> SpotMarketMap<'a> { Ok(SpotMarketMap(map, writable_markets)) } + pub fn empty() -> Self { + SpotMarketMap(BTreeMap::new(), BTreeSet::new()) + } + pub fn load_multiple<'c: 'a>( account_info: Vec<&'c AccountInfo<'a>>, must_be_writable: bool, From a0505022bb2116d3ab42eb503739b772eb871dfa Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Nov 2024 09:15:12 -0600 Subject: [PATCH 07/11] fix error message --- programs/drift/src/instructions/admin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index fc21c6b91..196bd0aec 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -330,7 +330,7 @@ pub fn handle_update_spot_market_pool_id( pool_id: u8, ) -> Result<()> { let mut spot_market = load_mut!(ctx.accounts.spot_market)?; - msg!("updating spot market {} expiry", spot_market.market_index); + msg!("updating spot market {} pool id to {}", spot_market.market_index, pool_id); validate!( spot_market.status == MarketStatus::Initialized, From a8727616b7e34d3c45279409a141af6c128e20e4 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Nov 2024 09:16:41 -0600 Subject: [PATCH 08/11] disable pools on mainnet --- programs/drift/src/instructions/admin.rs | 5 +++++ programs/drift/src/instructions/user.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 196bd0aec..211a835bc 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -329,6 +329,11 @@ pub fn handle_update_spot_market_pool_id( ctx: Context, pool_id: u8, ) -> Result<()> { + #[cfg(all(feature = "mainnet-beta", not(feature = "anchor-test")))] + { + panic!("pools disabled on mainnet-beta"); + } + let mut spot_market = load_mut!(ctx.accounts.spot_market)?; msg!("updating spot market {} pool id to {}", spot_market.market_index, pool_id); diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index ae4e7bad5..35179e578 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2221,6 +2221,11 @@ pub fn handle_update_user_pool_id<'c: 'info, 'info>( _sub_account_id: u16, pool_id: u8, ) -> Result<()> { + #[cfg(all(feature = "mainnet-beta", not(feature = "anchor-test")))] + { + panic!("pools disabled on mainnet-beta"); + } + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, From e17a730ce6de2ba5ea3a1e41aa563ea36bf63177 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Nov 2024 09:20:07 -0600 Subject: [PATCH 09/11] fix linters --- programs/drift/src/instructions/admin.rs | 6 +++++- sdk/src/driftClient.ts | 5 +---- tests/spotMarketPoolIds.ts | 26 +++++------------------- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 211a835bc..d1feb9054 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -335,7 +335,11 @@ pub fn handle_update_spot_market_pool_id( } let mut spot_market = load_mut!(ctx.accounts.spot_market)?; - msg!("updating spot market {} pool id to {}", spot_market.market_index, pool_id); + msg!( + "updating spot market {} pool id to {}", + spot_market.market_index, + pool_id + ); validate!( spot_market.status == MarketStatus::Initialized, diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 2137f60ac..1b4c1099f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1455,10 +1455,7 @@ export class DriftClient { return txSig; } - public async getUpdateUserPoolIdIx( - poolId: number, - subAccountId: number - ) { + public async getUpdateUserPoolIdIx(poolId: number, subAccountId: number) { const ix = await this.program.instruction.updateUserPoolId( subAccountId, poolId, diff --git a/tests/spotMarketPoolIds.ts b/tests/spotMarketPoolIds.ts index f74934f49..1240dcca8 100644 --- a/tests/spotMarketPoolIds.ts +++ b/tests/spotMarketPoolIds.ts @@ -3,43 +3,28 @@ import { assert } from 'chai'; import { Program } from '@coral-xyz/anchor'; -import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; +import { PublicKey } from '@solana/web3.js'; import { TestClient, BN, EventSubscriber, SPOT_MARKET_RATE_PRECISION, - SpotBalanceType, - isVariant, OracleSource, SPOT_MARKET_WEIGHT_PRECISION, - SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION, OracleInfo, MarketStatus, } from '../sdk/src'; import { createUserWithUSDCAccount, - createUserWithUSDCAndWSOLAccount, - mintUSDCToUser, mockOracleNoProgram, mockUSDCMint, mockUserUSDCAccount, sleep, } from './testHelpers'; -import { - getBalance, - calculateInterestAccumulated, - getTokenAmount, -} from '../sdk/src/math/spotBalance'; -import { NATIVE_MINT } from '@solana/spl-token'; import { QUOTE_PRECISION, - ZERO, - ONE, - SPOT_MARKET_BALANCE_PRECISION, - PRICE_PRECISION, } from '../sdk'; import { startAnchor } from 'solana-bankrun'; import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; @@ -63,14 +48,11 @@ describe('spot deposit and withdraw', () => { let firstUserDriftClientUSDCAccount: PublicKey; let secondUserDriftClient: TestClient; - let secondUserDriftClientWSOLAccount: PublicKey; let secondUserDriftClientUSDCAccount: PublicKey; const usdcAmount = new BN(10 * 10 ** 6); const largeUsdcAmount = new BN(10_000 * 10 ** 6); - const solAmount = new BN(1 * 10 ** 9); - let marketIndexes: number[]; let spotMarketIndexes: number[]; let oracleInfos: OracleInfo[]; @@ -175,7 +157,7 @@ describe('spot deposit and withdraw', () => { undefined, undefined, undefined, - false, + false ); await admin.updateSpotMarketPoolId(1, 1); await admin.updateSpotMarketStatus(1, MarketStatus.ACTIVE); @@ -235,7 +217,9 @@ describe('spot deposit and withdraw', () => { bulkAccountLoader ); - await secondUserDriftClient.updateUserPoolId([{subAccountId: 0, poolId: 1}]); + await secondUserDriftClient.updateUserPoolId([ + { subAccountId: 0, poolId: 1 }, + ]); await sleep(100); await secondUserDriftClient.fetchAccounts(); await secondUserDriftClient.deposit( From 745a3f78eb1da0a87c4788c7033dc38227197808 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Nov 2024 09:21:18 -0600 Subject: [PATCH 10/11] CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f0efab4..a35cc366f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add spot market pool ids ([#1250](https://github.com/drift-labs/protocol-v2/pull/1250)) + ### Fixes ### Breaking From 1585c9549246a0836f0ee3364282c27d627644e3 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Nov 2024 09:24:36 -0600 Subject: [PATCH 11/11] prettify 2 --- tests/spotMarketPoolIds.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/spotMarketPoolIds.ts b/tests/spotMarketPoolIds.ts index 1240dcca8..6b5a66073 100644 --- a/tests/spotMarketPoolIds.ts +++ b/tests/spotMarketPoolIds.ts @@ -23,9 +23,7 @@ import { mockUserUSDCAccount, sleep, } from './testHelpers'; -import { - QUOTE_PRECISION, -} from '../sdk'; +import { QUOTE_PRECISION } from '../sdk'; import { startAnchor } from 'solana-bankrun'; import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection';