From 51455be7f5d9e37dfe9441f810931936ca5656fa Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 25 Apr 2025 09:32:58 -0700 Subject: [PATCH 1/7] program: add admin_deposit --- programs/drift/src/instructions/admin.rs | 193 ++++++++++++++++++++++- programs/drift/src/lib.rs | 8 + programs/drift/src/state/events.rs | 1 + sdk/src/adminClient.ts | 52 ++++++ sdk/src/idl/drift.json | 48 ++++++ 5 files changed, 300 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index debb7ef2b..71cb3db6a 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,6 +1,7 @@ use std::convert::identity; use std::mem::size_of; +use crate::math::liquidation::is_user_being_liquidated; use crate::msg; use anchor_lang::prelude::*; use anchor_spl::token::Token; @@ -33,7 +34,9 @@ use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::math::{amm, bn}; use crate::optional_accounts::get_token_mint; -use crate::state::events::{CurveRecord, SpotMarketVaultDepositRecord}; +use crate::state::events::{ + CurveRecord, DepositDirection, DepositExplanation, DepositRecord, SpotMarketVaultDepositRecord, +}; use crate::state::fulfillment_params::openbook_v2::{ OpenbookV2Context, OpenbookV2FulfillmentConfig, }; @@ -54,7 +57,7 @@ use crate::state::paused_operations::{InsuranceFundOperation, PerpOperation, Spo use crate::state::perp_market::{ ContractTier, ContractType, InsuranceClaim, MarketStatus, PerpMarket, PoolBalance, AMM, }; -use crate::state::perp_market_map::get_writable_perp_market_set; +use crate::state::perp_market_map::{get_writable_perp_market_set, MarketSet}; use crate::state::protected_maker_mode_config::ProtectedMakerModeConfig; use crate::state::pyth_lazer_oracle::{PythLazerOracle, PYTH_LAZER_ORACLE_SEED}; use crate::state::spot_market::{ @@ -4438,6 +4441,169 @@ pub fn handle_update_protected_maker_mode_config( Ok(()) } +#[access_control( + deposit_not_paused(&ctx.accounts.state) +)] +pub fn handle_admin_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, AdminDeposit<'info>>, + market_index: u16, + amount: u64, +) -> Result<()> { + let user_key = ctx.accounts.user.key(); + let user = &mut load_mut!(ctx.accounts.user)?; + + let state = &ctx.accounts.state; + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let slot = clock.slot; + + 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(), + &get_writable_spot_market_set(market_index), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let mint = get_token_mint(remaining_accounts_iter)?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + let mut spot_market = spot_market_map.get_ref_mut(&market_index)?; + let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle_id())?; + + 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, + "Market is being initialized" + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_price_data), + now, + )?; + + let position_index = user.force_get_spot_position_index(spot_market.market_index)?; + + // if reduce only, have to compare ix amount to current borrow amount + let amount = if (spot_market.is_reduce_only()) + && user.spot_positions[position_index].balance_type == SpotBalanceType::Borrow + { + user.spot_positions[position_index] + .get_token_amount(&spot_market)? + .cast::()? + .min(amount) + } else { + amount + }; + + let total_deposits_after = user.total_deposits; + let total_withdraws_after = user.total_withdraws; + + let spot_position = &mut user.spot_positions[position_index]; + controller::spot_position::update_spot_balances_and_cumulative_deposits( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + spot_position, + false, + None, + )?; + + let token_amount = spot_position.get_token_amount(&spot_market)?; + if token_amount == 0 { + validate!( + spot_position.scaled_balance == 0, + ErrorCode::InvalidSpotPosition, + "deposit left user with invalid position. scaled balance = {} token amount = {}", + spot_position.scaled_balance, + token_amount + )?; + } + + if spot_position.balance_type == SpotBalanceType::Deposit && spot_position.scaled_balance > 0 { + validate!( + matches!(spot_market.status, MarketStatus::Active), + ErrorCode::MarketActionPaused, + "spot_market not active", + )?; + } + + drop(spot_market); + if user.is_being_liquidated() { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_user_being_liquidated( + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_liquidation(); + } + } + + user.update_last_active_slot(slot); + + let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.admin_token_account, + &ctx.accounts.spot_market_vault, + &ctx.accounts.admin, + amount, + &mint, + )?; + ctx.accounts.spot_market_vault.reload()?; + + let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); + let oracle_price = oracle_price_data.price; + let deposit_record = DepositRecord { + ts: now, + deposit_record_id, + user_authority: user.authority, + user: user_key, + direction: DepositDirection::Deposit, + amount, + oracle_price, + market_deposit_balance: spot_market.deposit_balance, + market_withdraw_balance: spot_market.borrow_balance, + market_cumulative_deposit_interest: spot_market.cumulative_deposit_interest, + market_cumulative_borrow_interest: spot_market.cumulative_borrow_interest, + total_deposits_after, + total_withdraws_after, + market_index, + explanation: DepositExplanation::Reward, + transfer_user: None, + }; + emit!(deposit_record); + + spot_market.validate_max_token_deposits_and_borrows(false)?; + + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5163,3 +5329,26 @@ pub struct UpdateProtectedMakerModeConfig<'info> { )] pub state: Box>, } + +#[derive(Accounts)] +#[instruction(market_index: u16,)] +pub struct AdminDeposit<'info> { + pub state: Box>, + #[account(mut)] + pub user: AccountLoader<'info, User>, + #[account(mut)] + pub admin: Signer<'info>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + constraint = &spot_market_vault.mint.eq(&admin_token_account.mint), + token::authority = admin.key() + )] + pub admin_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index e49cbea6b..8dda496c7 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1698,6 +1698,14 @@ pub mod drift { ) -> Result<()> { handle_update_protected_maker_mode_config(ctx, max_users, reduce_only, current_users) } + + pub fn admin_deposit<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, AdminDeposit<'info>>, + market_index: u16, + amount: u64, + ) -> Result<()> { + handle_admin_deposit(ctx, market_index, amount) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index aee150991..e99fe6a81 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -60,6 +60,7 @@ pub enum DepositExplanation { Transfer, Borrow, RepayBorrow, + Reward, } #[event] diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index c6637337d..84e6ba968 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -14,6 +14,7 @@ import { ContractTier, AssetTier, SpotFulfillmentConfigStatus, + UserAccount, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -4224,4 +4225,55 @@ export class AdminClient extends DriftClient { } ); } + + public async adminDeposit( + marketIndex: number, + amount: BN, + depositUserAccount: UserAccount, + adminTokenAccount?: PublicKey + ): Promise { + const ix = await this.getAdminDepositIx( + marketIndex, + amount, + depositUserAccount, + adminTokenAccount + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getAdminDepositIx( + marketIndex: number, + amount: BN, + depositUserAccount: UserAccount, + adminTokenAccount?: PublicKey + ): Promise { + const state = await this.getStatePublicKey(); + + const spotMarketVault = await getSpotMarketVaultPublicKey( + this.program.programId, + marketIndex + ); + + return this.program.instruction.adminDeposit(marketIndex, amount, { + remainingAccounts: this.getRemainingAccounts({ + userAccounts: [depositUserAccount], + writableSpotMarketIndexes: [marketIndex], + }), + accounts: { + state, + user: await this.getUserAccountPublicKey( + depositUserAccount.subAccountId, + depositUserAccount.authority + ), + admin: this.wallet.publicKey, + spotMarketVault, + adminTokenAccount: + adminTokenAccount ?? + (await this.getAssociatedTokenAccount(marketIndex)), + tokenProgram: TOKEN_PROGRAM_ID, + }, + }); + } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 85d8928b0..5aa1a0c30 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7096,6 +7096,51 @@ } } ] + }, + { + "name": "adminDeposit", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "adminTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] } ], "accounts": [ @@ -11398,6 +11443,9 @@ }, { "name": "RepayBorrow" + }, + { + "name": "Reward" } ] } From 63e2bb184814dad301afcbeb564cc7d7e60007a9 Mon Sep 17 00:00:00 2001 From: wphan Date: Fri, 25 Apr 2025 11:42:04 -0700 Subject: [PATCH 2/7] add admin key to event --- 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 71cb3db6a..e52174c75 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4595,7 +4595,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( total_withdraws_after, market_index, explanation: DepositExplanation::Reward, - transfer_user: None, + transfer_user: Some(ctx.accounts.admin.key()), }; emit!(deposit_record); From 422945d25504d029271c0abb05f62de515ae04c3 Mon Sep 17 00:00:00 2001 From: wphan Date: Sun, 27 Apr 2025 13:27:22 -0700 Subject: [PATCH 3/7] review feedback --- programs/drift/src/instructions/admin.rs | 22 ++++++---------------- sdk/src/adminClient.ts | 11 ++++------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index e52174c75..b47600118 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4459,7 +4459,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { - perp_market_map, + perp_market_map: _, spot_market_map, mut oracle_map, } = load_maps( @@ -4548,20 +4548,6 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( } drop(spot_market); - if user.is_being_liquidated() { - // try to update liquidation status if user is was already being liq'd - let is_being_liquidated = is_user_being_liquidated( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - state.liquidation_margin_buffer_ratio, - )?; - - if !is_being_liquidated { - user.exit_liquidation(); - } - } user.update_last_active_slot(slot); @@ -4576,6 +4562,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( &mint, )?; ctx.accounts.spot_market_vault.reload()?; + validate_spot_market_vault_amount(spot_market, ctx.accounts.spot_market_vault.amount)?; let deposit_record_id = get_then_update_id!(spot_market, next_deposit_record_id); let oracle_price = oracle_price_data.price; @@ -5336,7 +5323,10 @@ pub struct AdminDeposit<'info> { pub state: Box>, #[account(mut)] pub user: AccountLoader<'info, User>, - #[account(mut)] + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] pub admin: Signer<'info>, #[account( mut, diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 84e6ba968..ee67405de 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4229,7 +4229,7 @@ export class AdminClient extends DriftClient { public async adminDeposit( marketIndex: number, amount: BN, - depositUserAccount: UserAccount, + depositUserAccount: PublicKey, adminTokenAccount?: PublicKey ): Promise { const ix = await this.getAdminDepositIx( @@ -4246,7 +4246,7 @@ export class AdminClient extends DriftClient { public async getAdminDepositIx( marketIndex: number, amount: BN, - depositUserAccount: UserAccount, + depositUserAccount: PublicKey, adminTokenAccount?: PublicKey ): Promise { const state = await this.getStatePublicKey(); @@ -4258,15 +4258,12 @@ export class AdminClient extends DriftClient { return this.program.instruction.adminDeposit(marketIndex, amount, { remainingAccounts: this.getRemainingAccounts({ - userAccounts: [depositUserAccount], + userAccounts: [], writableSpotMarketIndexes: [marketIndex], }), accounts: { state, - user: await this.getUserAccountPublicKey( - depositUserAccount.subAccountId, - depositUserAccount.authority - ), + user: depositUserAccount, admin: this.wallet.publicKey, spotMarketVault, adminTokenAccount: From e4716d71dbda42c287d126370a01ad8838b7388a Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 28 Apr 2025 11:56:30 -0700 Subject: [PATCH 4/7] add pos and neg test --- programs/drift/src/instructions/admin.rs | 1 - test-scripts/single-anchor-test.sh | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index b47600118..1852feaf5 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,7 +1,6 @@ use std::convert::identity; use std::mem::size_of; -use crate::math::liquidation::is_user_being_liquidated; use crate::msg; use anchor_lang::prelude::*; use anchor_spl::token::Token; diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 38b880342..be7df6b86 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,8 +6,8 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(fuelSweep.ts) +test_files=(adminDeposit.ts) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} -done \ No newline at end of file +done From 0c14c68960682c034a1ffadedc8e54804adaa514 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 28 Apr 2025 14:09:28 -0700 Subject: [PATCH 5/7] remove admin from transfer_user field in event --- 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 1852feaf5..21eae00b2 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4581,7 +4581,7 @@ pub fn handle_admin_deposit<'c: 'info, 'info>( total_withdraws_after, market_index, explanation: DepositExplanation::Reward, - transfer_user: Some(ctx.accounts.admin.key()), + transfer_user: None, }; emit!(deposit_record); From e133bfc37ae82116fd87ac1a3f6ff1f4e78544de Mon Sep 17 00:00:00 2001 From: wphan Date: Tue, 29 Apr 2025 08:00:39 -0700 Subject: [PATCH 6/7] linter --- sdk/src/adminClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index ee67405de..50e233c27 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -14,7 +14,6 @@ import { ContractTier, AssetTier, SpotFulfillmentConfigStatus, - UserAccount, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; From c5519c95b5b8ad9493822a722a442820f56ffffa Mon Sep 17 00:00:00 2001 From: Nick Caradonna Date: Tue, 29 Apr 2025 11:57:00 -0400 Subject: [PATCH 7/7] add enum to types.ts --- sdk/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 497ee5f1b..e14fb57ba 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -275,6 +275,7 @@ export class DepositExplanation { static readonly TRANSFER = { transfer: {} }; static readonly BORROW = { borrow: {} }; static readonly REPAY_BORROW = { repayBorrow: {} }; + static readonly REWARD = { reward: {} }; } export class SettlePnlExplanation {