From 055582a7e1c2407deb7f6e7d57fb42afd87e9721 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 20 Nov 2024 16:22:13 -0500 Subject: [PATCH 01/11] program: init force delete user --- programs/drift/src/instructions/keeper.rs | 221 +++++++++++++++++++- programs/drift/src/lib.rs | 6 + programs/drift/src/state/perp_market_map.rs | 16 +- 3 files changed, 235 insertions(+), 8 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 33101e533..f2d266997 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,7 +12,10 @@ 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::instructions::constraints::*; @@ -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::{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,183 @@ 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(), + &get_market_set_for_spot_positions(&user.spot_positions), + &MarketSet::new(), + 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 + )?; + + 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); + + Ok(()) +} + #[derive(Accounts)] pub struct FillOrder<'info> { pub state: Box>, @@ -2588,3 +2773,25 @@ 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 + pub authority: AccountInfo<'info>, + 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/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 } From 85c703d432f50c7d22b3df466bf43ab1839a3dca Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 20 Nov 2024 17:11:00 -0500 Subject: [PATCH 02/11] init client work --- sdk/src/driftClient.ts | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 8daf7550f..99f966800 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1580,6 +1580,82 @@ 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, + }); + + 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 + ); + remainingAccounts.push({ + isSigner: false, + isWritable: true, + pubkey: keeperVault, + }); + if (spotMarket.tokenProgram > 0) { + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); + remainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: 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(), + }, + }); + + return ix; + } + public async deleteSwiftUserOrders( subAccountId = 0, txParams?: TxParams From d6e51a4a9df1b4b9a54054cc24462f89abff4b46 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 20 Nov 2024 18:16:27 -0500 Subject: [PATCH 03/11] test passing --- programs/drift/src/instructions/keeper.rs | 21 +- sdk/src/driftClient.ts | 24 +- sdk/src/idl/drift.json | 39 ++- test-scripts/single-anchor-test.sh | 2 +- tests/forceUserDelete.ts | 289 ++++++++++++++++++++++ 5 files changed, 356 insertions(+), 19 deletions(-) create mode 100644 tests/forceUserDelete.ts diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index f2d266997..a2cb16882 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2151,8 +2151,8 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( mut oracle_map, } = load_maps( &mut ctx.remaining_accounts.iter().peekable(), - &get_market_set_for_spot_positions(&user.spot_positions), &MarketSet::new(), + &get_market_set_for_spot_positions(&user.spot_positions), slot, Some(state.oracle_guard_rails), )?; @@ -2170,14 +2170,17 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( max_equity )?; - let slots_since_last_active = slot.safe_sub(user.last_active_slot)?; + #[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 - )?; + 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( @@ -2790,7 +2793,9 @@ pub struct ForceDeleteUser<'info> { #[account(mut)] pub state: Box>, /// CHECK: authority + #[account(mut)] pub authority: AccountInfo<'info>, + #[account(mut)] pub keeper: Signer<'info>, /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 99f966800..752e20cfc 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1610,6 +1610,7 @@ export class DriftClient { writableSpotMarketIndexes, }); + const tokenPrograms = new Set(); for (const spotPosition of userAccount.spotPositions) { if (isSpotPositionAvailable(spotPosition)) { continue; @@ -1621,21 +1622,24 @@ export class DriftClient { pubkey: spotMarket.vault, }); const keeperVault = await this.getAssociatedTokenAccount( - spotPosition.marketIndex + spotPosition.marketIndex, + false ); remainingAccounts.push({ isSigner: false, isWritable: true, pubkey: keeperVault, }); - if (spotMarket.tokenProgram > 0) { - const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); - remainingAccounts.push({ - isSigner: false, - isWritable: false, - pubkey: tokenProgram, - }); - } + 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; @@ -1650,7 +1654,9 @@ export class DriftClient { authority, state: await this.getStatePublicKey(), driftSigner: this.getSignerPublicKey(), + keeper: this.wallet.publicKey, }, + remainingAccounts, }); return ix; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 7ea243132..a9ce27677 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" ] }, { 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..c5cb94a4d --- /dev/null +++ b/tests/forceUserDelete.ts @@ -0,0 +1,289 @@ +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, +} from '../sdk/src'; + +import { + createUSDCAccountForUser, + createUserWithUSDCAccount, + createUserWithUSDCAndWSOLAccount, + createWSolTokenAccountForUser, + mintUSDCToUser, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + sleep, +} from './testHelpers'; +import { + getBalance, + calculateInterestAccumulated, + getTokenAmount, +} from '../sdk/src/math/spotBalance'; +import { createAssociatedTokenAccountIdempotent, createSyncNativeInstruction, 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 firstUserDriftClientWSOLAccount: PublicKey; + let firstUserDriftClientUSDCAccount: PublicKey; + + let secondUserDriftClient: TestClient; + let secondUserDriftClientWSOLAccount: PublicKey; + let secondUserDriftClientUSDCAccount: 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, + 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); + }); + + 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)); + }); +}); From b2eb43dd487f77e9249ac48223d3c8acffedcb2d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 08:01:06 -0500 Subject: [PATCH 04/11] add event --- programs/drift/src/instructions/keeper.rs | 11 ++++++++++- programs/drift/src/state/events.rs | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index a2cb16882..0352792a2 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -28,7 +28,7 @@ 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::{OrderActionExplanation, 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; @@ -2309,6 +2309,15 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( let state = &mut ctx.accounts.state; safe_decrement!(state.number_of_sub_accounts, 1); + // todo emit record + 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(()) } 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]; From 3f5384e508421d31aba3e03df8d48d979ff0b515 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 08:04:25 -0500 Subject: [PATCH 05/11] add event to sdk --- sdk/src/events/types.ts | 6 +++++- sdk/src/types.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) 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/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; From 8ecd0ce9e33518dac5c1fb808025d27256ba72d1 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 08:09:30 -0500 Subject: [PATCH 06/11] add assert account is actually deleted --- sdk/src/idl/drift.json | 32 ++++++++++++++++++++++++++++++++ tests/forceUserDelete.ts | 3 +++ 2 files changed, 35 insertions(+) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index a9ce27677..4abd09aed 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -12545,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/tests/forceUserDelete.ts b/tests/forceUserDelete.ts index c5cb94a4d..95c4307e3 100644 --- a/tests/forceUserDelete.ts +++ b/tests/forceUserDelete.ts @@ -285,5 +285,8 @@ describe('spot deposit and withdraw', () => { )); // @ts-ignore await secondUserDriftClient.sendTransaction(await secondUserDriftClient.buildTransaction(ixs)); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo(await firstUserDriftClient.getUserAccountPublicKey()); + assert(accountInfo === null); }); }); From 2940bad527941a7e7689ed29b9a3836897e66487 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 08:10:57 -0500 Subject: [PATCH 07/11] rm unused dependencies --- tests/forceUserDelete.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/forceUserDelete.ts b/tests/forceUserDelete.ts index 95c4307e3..7fd90810b 100644 --- a/tests/forceUserDelete.ts +++ b/tests/forceUserDelete.ts @@ -10,37 +10,23 @@ import { BN, EventSubscriber, SPOT_MARKET_RATE_PRECISION, - SpotBalanceType, - isVariant, OracleSource, SPOT_MARKET_WEIGHT_PRECISION, - SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION, OracleInfo, } from '../sdk/src'; import { - createUSDCAccountForUser, - createUserWithUSDCAccount, createUserWithUSDCAndWSOLAccount, createWSolTokenAccountForUser, - mintUSDCToUser, mockOracleNoProgram, mockUSDCMint, mockUserUSDCAccount, sleep, } from './testHelpers'; -import { - getBalance, - calculateInterestAccumulated, - getTokenAmount, -} from '../sdk/src/math/spotBalance'; -import { createAssociatedTokenAccountIdempotent, createSyncNativeInstruction, NATIVE_MINT } from '@solana/spl-token'; +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'; From 4a490109a0f97cc688d9bd2581b6301ef44013b7 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 13:40:00 -0500 Subject: [PATCH 08/11] only hot wallet can delete users --- programs/drift/src/instructions/keeper.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 0352792a2..5b5f11d9c 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -17,7 +17,7 @@ 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; @@ -2804,7 +2804,10 @@ pub struct ForceDeleteUser<'info> { /// CHECK: authority #[account(mut)] pub authority: AccountInfo<'info>, - #[account(mut)] + #[account( + mut, + constraint = keeper.key() == admin_hot_wallet::id() + )] pub keeper: Signer<'info>, /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, From ae184f5f20483689475b2a9f0b446fc94f34c799 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 13:41:09 -0500 Subject: [PATCH 09/11] rm comment --- programs/drift/src/instructions/keeper.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 5b5f11d9c..6e4528f7e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2309,7 +2309,6 @@ pub fn handle_force_delete_user<'c: 'info, 'info>( let state = &mut ctx.accounts.state; safe_decrement!(state.number_of_sub_accounts, 1); - // todo emit record emit!(DeleteUserRecord { ts: now, user_authority: *ctx.accounts.authority.key, From e3cc71f2b1faa08be0bf364e0a4e15491e07ff01 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 13:52:48 -0500 Subject: [PATCH 10/11] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From 52e9232ae337baf99bfcfa132ea907249df9c3cd Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Nov 2024 13:57:31 -0500 Subject: [PATCH 11/11] fix prettify/lint --- tests/forceUserDelete.ts | 90 +++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/tests/forceUserDelete.ts b/tests/forceUserDelete.ts index 7fd90810b..51a91cd99 100644 --- a/tests/forceUserDelete.ts +++ b/tests/forceUserDelete.ts @@ -24,10 +24,7 @@ import { sleep, } from './testHelpers'; import { NATIVE_MINT } from '@solana/spl-token'; -import { - QUOTE_PRECISION, - ZERO, -} from '../sdk'; +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'; @@ -47,12 +44,11 @@ describe('spot deposit and withdraw', () => { let usdcMint; let firstUserDriftClient: TestClient; - let firstUserDriftClientWSOLAccount: PublicKey; + let firstUserDriftClientWSOLAccount: PublicKey; let firstUserDriftClientUSDCAccount: PublicKey; let secondUserDriftClient: TestClient; let secondUserDriftClientWSOLAccount: PublicKey; - let secondUserDriftClientUSDCAccount: PublicKey; const usdcAmount = new BN(10 ** 6 / 20); const largeUsdcAmount = new BN(10_000 * 10 ** 6); @@ -192,18 +188,21 @@ describe('spot deposit and withdraw', () => { }); it('First User Deposit USDC', async () => { - [firstUserDriftClient, firstUserDriftClientWSOLAccount, firstUserDriftClientUSDCAccount] = - await createUserWithUSDCAndWSOLAccount( - bankrunContextWrapper, - usdcMint, - chProgram, - ZERO, - usdcAmount, - marketIndexes, - spotMarketIndexes, - oracleInfos, - bulkAccountLoader - ); + [ + firstUserDriftClient, + firstUserDriftClientWSOLAccount, + firstUserDriftClientUSDCAccount, + ] = await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + ZERO, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); const marketIndex = 0; await sleep(100); @@ -220,7 +219,6 @@ describe('spot deposit and withdraw', () => { [ secondUserDriftClient, secondUserDriftClientWSOLAccount, - secondUserDriftClientUSDCAccount, ] = await createUserWithUSDCAndWSOLAccount( bankrunContextWrapper, usdcMint, @@ -254,25 +252,39 @@ describe('spot deposit and withdraw', () => { }); 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); + 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); }); });