diff --git a/CHANGELOG.md b/CHANGELOG.md index aea49f957..065a97e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: force delete user init ([#1341](https://github.com/drift-labs/protocol-v2/pull/1341)) - program: rm withdraw fee ([#1334](https://github.com/drift-labs/protocol-v2/pull/1334)) ### Fixes diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 33101e533..6e4528f7e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,6 +1,10 @@ use std::cell::RefMut; +use std::convert::TryFrom; use anchor_lang::prelude::*; +use anchor_spl::associated_token::get_associated_token_address; +use anchor_spl::token::spl_token; +use anchor_spl::token_2022::spl_token_2022; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use solana_program::instruction::Instruction; use solana_program::sysvar::instructions::{ @@ -8,9 +12,12 @@ use solana_program::sysvar::instructions::{ }; use crate::controller::insurance::update_user_stats_if_stake_amount; +use crate::controller::orders::cancel_orders; use crate::controller::position::PositionDirection; +use crate::controller::spot_balance::update_spot_balances; +use crate::controller::token::{receive, send_from_program_vault}; use crate::error::ErrorCode; -use crate::ids::swift_server; +use crate::ids::{admin_hot_wallet, swift_server}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; @@ -19,8 +26,9 @@ use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_ma use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::safe_math::SafeMath; use crate::math::spot_withdraw::validate_spot_market_vault_amount; +use crate::math_error; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; -use crate::state::events::SwiftOrderRecord; +use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SwiftOrderRecord}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment_params::drift::MatchFulfillmentParams; use crate::state::fulfillment_params::openbook_v2::OpenbookV2FulfillmentParams; @@ -35,12 +43,12 @@ use crate::state::order_params::{ use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::{ContractType, MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ - get_market_set_for_user_positions, get_market_set_from_list, get_writable_perp_market_set, - get_writable_perp_market_set_from_vec, MarketSet, PerpMarketMap, + get_market_set_for_spot_positions, get_market_set_for_user_positions, get_market_set_from_list, + get_writable_perp_market_set, get_writable_perp_market_set_from_vec, MarketSet, PerpMarketMap, }; use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::spot_fulfillment_params::SpotFulfillmentParams; -use crate::state::spot_market::SpotMarket; +use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::{ get_writable_spot_market_set, get_writable_spot_market_set_from_many, SpotMarketMap, }; @@ -53,10 +61,10 @@ use crate::state::user::{ }; use crate::state::user_map::{load_user_map, load_user_maps, UserMap, UserStatsMap}; use crate::validation::sig_verification::{extract_ed25519_ix_signature, verify_ed25519_digest}; -use crate::validation::user::validate_user_is_idle; +use crate::validation::user::{validate_user_deletion, validate_user_is_idle}; use crate::{ - controller, digest_struct, load, math, print_error, OracleSource, GOV_SPOT_MARKET_INDEX, - MARGIN_PRECISION, + controller, digest_struct, load, math, print_error, safe_decrement, OracleSource, + GOV_SPOT_MARKET_INDEX, MARGIN_PRECISION, }; use crate::{load_mut, QUOTE_PRECISION_U64}; use crate::{validate, QUOTE_PRECISION_I128}; @@ -2124,6 +2132,194 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( Ok(()) } +pub fn handle_force_delete_user<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ForceDeleteUser<'info>>, +) -> Result<()> { + let state = &ctx.accounts.state; + + let keeper_key = *ctx.accounts.keeper.key; + + let user_key = ctx.accounts.user.key(); + let user = &mut load_mut!(ctx.accounts.user)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &get_market_set_for_spot_positions(&user.spot_positions), + slot, + Some(state.oracle_guard_rails), + )?; + + // check the user equity + + let (user_equity, _) = + calculate_user_equity(user, &perp_market_map, &spot_market_map, &mut oracle_map)?; + + let max_equity = QUOTE_PRECISION_I128 / 20; + validate!( + user_equity <= max_equity, + ErrorCode::DefaultError, + "user equity must be less than {}", + max_equity + )?; + + #[cfg(not(feature = "anchor-test"))] + { + let slots_since_last_active = slot.safe_sub(user.last_active_slot)?; + + validate!( + slots_since_last_active >= 18144000, // 60 * 60 * 24 * 7 * 4 * 3 / .4 (~3 months) + ErrorCode::DefaultError, + "user not inactive for long enough: {}", + slots_since_last_active + )?; + } + + // cancel all open orders + let canceled_order_ids = cancel_orders( + user, + &user_key, + Some(&keeper_key), + &perp_market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + OrderActionExplanation::None, + None, + None, + None, + )?; + + for spot_position in user.spot_positions.iter_mut() { + if spot_position.is_available() { + continue; + } + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_position.market_index)?; + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle)?; + + controller::spot_balance::update_spot_market_cumulative_interest( + spot_market, + Some(oracle_price_data), + now, + )?; + + let token_amount = spot_position.get_token_amount(spot_market)?; + let balance_type = spot_position.balance_type; + + let token_program_pubkey = if spot_market.token_program == 1 { + spl_token_2022::ID + } else { + spl_token::ID + }; + let token_program = &ctx + .remaining_accounts + .iter() + .find(|acc| acc.key() == token_program_pubkey) + .map(|acc| Interface::try_from(acc)) + .unwrap() + .unwrap(); + + let spot_market_mint = &spot_market.mint; + let mint_account_info = ctx + .remaining_accounts + .iter() + .find(|acc| acc.key() == spot_market_mint.key()) + .map(|acc| InterfaceAccount::try_from(acc).unwrap()); + + let keeper_vault = get_associated_token_address(&keeper_key, spot_market_mint); + let keeper_vault_account_info = ctx + .remaining_accounts + .iter() + .find(|acc| acc.key() == keeper_vault.key()) + .map(|acc| InterfaceAccount::try_from(acc)) + .unwrap() + .unwrap(); + + let spot_market_vault = spot_market.vault; + let mut spot_market_vault_account_info = ctx + .remaining_accounts + .iter() + .find(|acc| acc.key() == spot_market_vault.key()) + .map(|acc| InterfaceAccount::try_from(acc)) + .unwrap() + .unwrap(); + + if balance_type == SpotBalanceType::Deposit { + update_spot_balances( + token_amount, + &SpotBalanceType::Borrow, + spot_market, + spot_position, + true, + )?; + + send_from_program_vault( + &token_program, + &spot_market_vault_account_info, + &keeper_vault_account_info, + &ctx.accounts.drift_signer, + state.signer_nonce, + token_amount.cast()?, + &mint_account_info, + )?; + } else { + update_spot_balances( + token_amount, + &SpotBalanceType::Deposit, + spot_market, + spot_position, + false, + )?; + + receive( + token_program, + &keeper_vault_account_info, + &spot_market_vault_account_info, + &ctx.accounts.keeper.to_account_info(), + token_amount.cast()?, + &mint_account_info, + )?; + } + + spot_market_vault_account_info.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + spot_market, + spot_market_vault_account_info.amount, + )?; + } + + validate_user_deletion( + user, + user_stats, + &ctx.accounts.state, + Clock::get()?.unix_timestamp, + )?; + + safe_decrement!(user_stats.number_of_sub_accounts, 1); + + let state = &mut ctx.accounts.state; + safe_decrement!(state.number_of_sub_accounts, 1); + + emit!(DeleteUserRecord { + ts: now, + user_authority: *ctx.accounts.authority.key, + user: user_key, + sub_account_id: user.sub_account_id, + keeper: Some(*ctx.accounts.keeper.key), + }); + + Ok(()) +} + #[derive(Accounts)] pub struct FillOrder<'info> { pub state: Box>, @@ -2588,3 +2784,30 @@ pub struct DisableUserHighLeverageMode<'info> { #[account(mut)] pub high_leverage_mode_config: AccountLoader<'info, HighLeverageModeConfig>, } + +#[derive(Accounts)] +pub struct ForceDeleteUser<'info> { + #[account( + mut, + has_one = authority, + close = authority + )] + pub user: AccountLoader<'info, User>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + #[account(mut)] + pub state: Box>, + /// CHECK: authority + #[account(mut)] + pub authority: AccountInfo<'info>, + #[account( + mut, + constraint = keeper.key() == admin_hot_wallet::id() + )] + pub keeper: Signer<'info>, + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index cd155502f..9927d10ff 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -349,6 +349,12 @@ pub mod drift { handle_delete_user(ctx) } + pub fn force_delete_user<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ForceDeleteUser<'info>>, + ) -> Result<()> { + handle_force_delete_user(ctx) + } + pub fn delete_swift_user_orders<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, DeleteSwiftUserOrders>, ) -> Result<()> { diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 5d4e75486..c94150bf9 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -582,6 +582,16 @@ pub struct SpotMarketVaultDepositRecord { pub amount: u64, } +#[event] +pub struct DeleteUserRecord { + /// unix_timestamp of action + pub ts: i64, + pub user_authority: Pubkey, + pub user: Pubkey, + pub sub_account_id: u16, + pub keeper: Option, +} + pub fn emit_stack(event: T) -> DriftResult { let mut data_buf = [0u8; N]; let mut out_buf = [0u8; N]; diff --git a/programs/drift/src/state/perp_market_map.rs b/programs/drift/src/state/perp_market_map.rs index 4990ac95b..de1beb5c7 100644 --- a/programs/drift/src/state/perp_market_map.rs +++ b/programs/drift/src/state/perp_market_map.rs @@ -18,6 +18,8 @@ use crate::state::traits::Size; use solana_program::msg; use std::panic::Location; +use super::user::SpotPosition; + pub struct PerpMarketMap<'a>(pub BTreeMap>); impl<'a> PerpMarketMap<'a> { @@ -247,7 +249,19 @@ pub fn get_market_set_from_list(market_indexes: [u16; 5]) -> MarketSet { pub fn get_market_set_for_user_positions(user_positions: &PerpPositions) -> MarketSet { let mut writable_markets = MarketSet::new(); for position in user_positions.iter() { - writable_markets.insert(position.market_index); + if !position.is_available() { + writable_markets.insert(position.market_index); + } + } + writable_markets +} + +pub fn get_market_set_for_spot_positions(spot_positions: &[SpotPosition]) -> MarketSet { + let mut writable_markets = MarketSet::new(); + for position in spot_positions.iter() { + if !position.is_available() { + writable_markets.insert(position.market_index); + } } writable_markets } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index f4e3a0e10..839db0271 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1580,6 +1580,88 @@ export class DriftClient { return ix; } + public async forceDeleteUser( + userAccountPublicKey: PublicKey, + userAccount: UserAccount, + txParams?: TxParams + ): Promise { + const tx = await this.buildTransaction( + await this.getForceDeleteUserIx(userAccountPublicKey, userAccount), + txParams + ); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getForceDeleteUserIx( + userAccountPublicKey: PublicKey, + userAccount: UserAccount + ) { + const writableSpotMarketIndexes = []; + for (const spotPosition of userAccount.spotPositions) { + if (isSpotPositionAvailable(spotPosition)) { + continue; + } + writableSpotMarketIndexes.push(spotPosition.marketIndex); + } + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [userAccount], + writableSpotMarketIndexes, + }); + + const tokenPrograms = new Set(); + for (const spotPosition of userAccount.spotPositions) { + if (isSpotPositionAvailable(spotPosition)) { + continue; + } + const spotMarket = this.getSpotMarketAccount(spotPosition.marketIndex); + remainingAccounts.push({ + isSigner: false, + isWritable: true, + pubkey: spotMarket.vault, + }); + const keeperVault = await this.getAssociatedTokenAccount( + spotPosition.marketIndex, + false + ); + remainingAccounts.push({ + isSigner: false, + isWritable: true, + pubkey: keeperVault, + }); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); + tokenPrograms.add(tokenProgram.toBase58()); + } + + for (const tokenProgram of tokenPrograms) { + remainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: new PublicKey(tokenProgram), + }); + } + + const authority = userAccount.authority; + const userStats = getUserStatsAccountPublicKey( + this.program.programId, + authority + ); + const ix = await this.program.instruction.forceDeleteUser({ + accounts: { + user: userAccountPublicKey, + userStats, + authority, + state: await this.getStatePublicKey(), + driftSigner: this.getSignerPublicKey(), + keeper: this.wallet.publicKey, + }, + remainingAccounts, + }); + + return ix; + } + public async deleteSwiftUserOrders( subAccountId = 0, txParams?: TxParams diff --git a/sdk/src/events/types.ts b/sdk/src/events/types.ts index 7d3a6d1ae..aa1f360c0 100644 --- a/sdk/src/events/types.ts +++ b/sdk/src/events/types.ts @@ -16,6 +16,7 @@ import { SwapRecord, SpotMarketVaultDepositRecord, SwiftOrderRecord, + DeleteUserRecord, } from '../index'; import { EventEmitter } from 'events'; @@ -51,6 +52,7 @@ export const DefaultEventSubscriptionOptions: EventSubscriptionOptions = { 'SwapRecord', 'SpotMarketVaultDepositRecord', 'SwiftOrderRecord', + 'DeleteUserRecord', ], maxEventsPerType: 4096, orderBy: 'blockchain', @@ -95,6 +97,7 @@ export type EventMap = { SwapRecord: Event; SpotMarketVaultDepositRecord: Event; SwiftOrderRecord: Event; + DeleteUserRecord: Event; }; export type EventType = keyof EventMap; @@ -115,7 +118,8 @@ export type DriftEvent = | Event | Event | Event - | Event; + | Event + | Event; export interface EventSubscriberEvents { newEvent: (event: WrappedEvent) => void; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 7ea243132..4abd09aed 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1458,6 +1458,42 @@ ], "args": [] }, + { + "name": "forceDeleteUser", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "deleteSwiftUserOrders", "accounts": [ @@ -10826,7 +10862,8 @@ { "name": "PlaceAndTake", "fields": [ - "bool" + "bool", + "u8" ] }, { @@ -12508,6 +12545,38 @@ "index": false } ] + }, + { + "name": "DeleteUserRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "userAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "subAccountId", + "type": "u16", + "index": false + }, + { + "name": "keeper", + "type": { + "option": "publicKey" + }, + "index": false + } + ] } ], "errors": [ diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 71493dccb..3e11c6f42 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -611,6 +611,14 @@ export type SpotMarketVaultDepositRecord = { amount: BN; }; +export type DeleteUserRecord = { + ts: BN; + userAuthority: PublicKey; + user: PublicKey; + subAccountId: number; + keeper: PublicKey | null; +}; + export type StateAccount = { admin: PublicKey; exchangeStatus: number; diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 00ae718d9..06fd3065d 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=(placeAndMakeSwiftPerpBankrun.ts) +test_files=(forceUserDelete.ts) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/forceUserDelete.ts b/tests/forceUserDelete.ts new file mode 100644 index 000000000..51a91cd99 --- /dev/null +++ b/tests/forceUserDelete.ts @@ -0,0 +1,290 @@ +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, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + OracleInfo, +} from '../sdk/src'; + +import { + createUserWithUSDCAndWSOLAccount, + createWSolTokenAccountForUser, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + sleep, +} from './testHelpers'; +import { NATIVE_MINT } from '@solana/spl-token'; +import { QUOTE_PRECISION, ZERO } 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 firstUserDriftClientWSOLAccount: PublicKey; + let firstUserDriftClientUSDCAccount: PublicKey; + + let secondUserDriftClient: TestClient; + let secondUserDriftClientWSOLAccount: PublicKey; + + const usdcAmount = new BN(10 ** 6 / 20); + 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 USDC Market', 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 + ); + const txSig = await admin.updateWithdrawGuardThreshold( + 0, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + bankrunContextWrapper.printTxLogs(txSig); + await admin.fetchAccounts(); + }); + + it('Initialize SOL Market', 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.mul(new BN(8)) + .div(new BN(10)) + .toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(9)) + .div(new BN(10)) + .toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(12)) + .div(new BN(10)) + .toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.mul( + new BN(11) + ) + .div(new BN(10)) + .toNumber(); + + await admin.initializeSpotMarket( + NATIVE_MINT, + optimalUtilization, + optimalRate, + maxRate, + solOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight + ); + + const txSig = await admin.updateWithdrawGuardThreshold( + 1, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + bankrunContextWrapper.printTxLogs(txSig); + await admin.fetchAccounts(); + }); + + it('First User Deposit USDC', async () => { + [ + firstUserDriftClient, + firstUserDriftClientWSOLAccount, + firstUserDriftClientUSDCAccount, + ] = await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + ZERO, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + const marketIndex = 0; + await sleep(100); + await firstUserDriftClient.fetchAccounts(); + const txSig = await firstUserDriftClient.deposit( + usdcAmount, + marketIndex, + firstUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + }); + + it('Second User Deposit SOL', async () => { + [ + secondUserDriftClient, + secondUserDriftClientWSOLAccount, + ] = 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); + }); + + it('First User Borrow SOL', async () => { + const marketIndex = 1; + const withdrawAmount = solAmount.div(new BN(1000)); + const txSig = await firstUserDriftClient.withdraw( + withdrawAmount, + marketIndex, + firstUserDriftClientWSOLAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + }); + + it('Force delete', async () => { + await firstUserDriftClient.fetchAccounts(); + // @ts-ignore + await createWSolTokenAccountForUser( + bankrunContextWrapper, + secondUserDriftClient.wallet, + new BN(LAMPORTS_PER_SOL) + ); + // @ts-ignore + await secondUserDriftClient.sendTransaction( + await secondUserDriftClient.buildTransaction([ + await secondUserDriftClient.createAssociatedTokenAccountIdempotentInstruction( + await secondUserDriftClient.getAssociatedTokenAccount(0), + secondUserDriftClient.wallet.publicKey, + secondUserDriftClient.wallet.publicKey, + secondUserDriftClient.getSpotMarketAccount(0).mint + ), + ]) + ); + const ixs = []; + ixs.push( + await secondUserDriftClient.getForceDeleteUserIx( + await firstUserDriftClient.getUserAccountPublicKey(), + await firstUserDriftClient.getUserAccount() + ) + ); + // @ts-ignore + await secondUserDriftClient.sendTransaction( + await secondUserDriftClient.buildTransaction(ixs) + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + await firstUserDriftClient.getUserAccountPublicKey() + ); + assert(accountInfo === null); + }); +});