diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index debb7ef2b..21eae00b2 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -33,7 +33,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 +56,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 +4440,156 @@ 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); + + 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()?; + 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; + 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 +5315,29 @@ 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, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + 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..50e233c27 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4224,4 +4224,52 @@ export class AdminClient extends DriftClient { } ); } + + public async adminDeposit( + marketIndex: number, + amount: BN, + depositUserAccount: PublicKey, + 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: PublicKey, + 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: [], + writableSpotMarketIndexes: [marketIndex], + }), + accounts: { + state, + user: depositUserAccount, + 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" } ] } 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 { 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