diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 04f98f7e89..7fe34a53d4 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -112,6 +112,7 @@ pub fn add_insurance_fund_stake( user_stats: &mut UserStats, spot_market: &mut SpotMarket, now: i64, + admin_deposit: bool, ) -> DriftResult { validate!( !(insurance_vault_amount == 0 && spot_market.insurance_fund.total_shares != 0), @@ -161,7 +162,11 @@ pub fn add_insurance_fund_stake( emit!(InsuranceFundStakeRecord { ts: now, user_authority: user_stats.authority, - action: StakeAction::Stake, + action: if admin_deposit { + StakeAction::AdminDeposit + } else { + StakeAction::Stake + }, amount, market_index: spot_market.market_index, insurance_vault_amount_before: insurance_vault_amount, diff --git a/programs/drift/src/controller/insurance/tests.rs b/programs/drift/src/controller/insurance/tests.rs index 4301a3d26f..c71948c3d3 100644 --- a/programs/drift/src/controller/insurance/tests.rs +++ b/programs/drift/src/controller/insurance/tests.rs @@ -41,6 +41,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -104,6 +105,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -141,6 +143,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -202,6 +205,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -245,6 +249,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -334,6 +339,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 20, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 199033744205760); @@ -346,6 +352,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 30, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 398067488411520); @@ -378,6 +385,7 @@ pub fn gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -502,6 +510,7 @@ pub fn losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -631,6 +640,7 @@ pub fn escrow_losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -729,7 +739,8 @@ pub fn escrow_gains_stake_if_test() { &mut if_stake, &mut user_stats, &mut spot_market, - 0 + 0, + false, ) .is_err()); @@ -741,6 +752,7 @@ pub fn escrow_gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -858,6 +870,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .is_err()); @@ -877,6 +890,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += amount; @@ -912,6 +926,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut orig_user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1010,6 +1025,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1210,6 +1226,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all_2() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += 10_000_000_000_000; @@ -1254,6 +1271,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1266,6 +1284,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); @@ -1392,6 +1411,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1404,6 +1424,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 51e4d7fd79..1d15b5ff6f 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -3,7 +3,7 @@ use anchor_lang::Discriminator; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use crate::error::ErrorCode; -use crate::ids::if_rebalance_wallet; +use crate::ids::{admin_hot_wallet, if_rebalance_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::optional_accounts::get_token_mint; @@ -143,6 +143,7 @@ pub fn handle_add_insurance_fund_stake<'c: 'info, 'info>( user_stats, spot_market, clock.unix_timestamp, + false, )?; controller::token::receive( @@ -821,6 +822,114 @@ pub fn handle_transfer_protocol_if_shares_to_revenue_pool<'c: 'info, 'info>( Ok(()) } +pub fn handle_deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, +) -> Result<()> { + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + let state = &ctx.accounts.state; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + validate!( + !spot_market.is_insurance_fund_operation_paused(InsuranceFundOperation::Add), + ErrorCode::InsuranceFundOperationPaused, + "if staking add disabled", + )?; + + validate!( + insurance_fund_stake.market_index == market_index, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake does not match market_index" + )?; + + validate!( + spot_market.status != MarketStatus::Initialized, + ErrorCode::InvalidSpotMarketState, + "spot market = {} not active for insurance_fund_stake", + spot_market.market_index + )?; + + validate!( + insurance_fund_stake.last_withdraw_request_shares == 0 + && insurance_fund_stake.last_withdraw_request_value == 0, + ErrorCode::IFWithdrawRequestInProgress, + "withdraw request in progress" + )?; + + { + if spot_market.has_transfer_hook() { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + Some(&mut remaining_accounts_iter.clone()), + )?; + } else { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + None, + )?; + }; + + // reload the vault balances so they're up-to-date + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.insurance_fund_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + } + + controller::insurance::add_insurance_fund_stake( + amount, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + clock.unix_timestamp, + true, + )?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.insurance_fund_vault, + &ctx.accounts.signer.to_account_info(), + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + Ok(()) +} + #[derive(Accounts)] #[instruction( market_index: u16, @@ -1082,3 +1191,49 @@ pub struct TransferProtocolIfSharesToRevenuePool<'info> { /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, } + +#[derive(Accounts)] +#[instruction(market_index: u16,)] +pub struct DepositIntoInsuranceFundStake<'info> { + pub signer: Signer<'info>, + #[account( + mut, + constraint = signer.key() == admin_hot_wallet::id() || signer.key() == state.admin + )] + pub state: Box>, + #[account( + mut, + seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + seeds = [b"insurance_fund_stake", user_stats.load()?.authority.as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + mut, + token::mint = insurance_fund_vault.mint, + token::authority = signer + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 517ddb2c86..81c7794515 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -813,6 +813,14 @@ pub mod drift { handle_transfer_protocol_if_shares_to_revenue_pool(ctx, market_index, amount) } + pub fn deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, + ) -> Result<()> { + handle_deposit_into_insurance_fund_stake(ctx, market_index, amount) + } + pub fn update_pyth_pull_oracle( ctx: Context, feed_id: [u8; 32], diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 55e9cecaeb..2d6c20fa7a 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -580,6 +580,7 @@ pub enum StakeAction { Unstake, UnstakeTransfer, StakeTransfer, + AdminDeposit, } #[event] diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 030677e254..a0234ff7ff 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -15,6 +15,7 @@ import { AssetTier, SpotFulfillmentConfigStatus, IfRebalanceConfigParams, + TxParams, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -4700,4 +4701,54 @@ export class AdminClient extends DriftClient { return ix; } + + public async depositIntoInsuranceFundStake( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey, + txParams?: TxParams + ): Promise { + const tx = await this.buildTransaction( + await this.getDepositIntoInsuranceFundStakeIx( + marketIndex, + amount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userTokenAccountPublicKey + ), + txParams + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositIntoInsuranceFundStakeIx( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey + ): Promise { + const spotMarket = this.getSpotMarketAccount(marketIndex); + return await this.program.instruction.depositIntoInsuranceFundStake( + marketIndex, + amount, + { + accounts: { + signer: this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: insuranceFundStakePublicKey, + userStats: userStatsPublicKey, + spotMarketVault: spotMarket.vault, + insuranceFundVault: spotMarket.insuranceFund.vault, + userTokenAccount: userTokenAccountPublicKey, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarket), + driftSigner: this.getSignerPublicKey(), + }, + } + ); + } } diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 6c25dcaf1c..bafefbacc5 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -3141,6 +3141,42 @@ ], "args": [] }, + { + "name": "updateDelegateUserGovTokenInsuranceStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializeInsuranceFundStake", "accounts": [ @@ -3634,6 +3670,71 @@ } ] }, + { + "name": "depositIntoInsuranceFundStake", + "accounts": [ + { + "name": "signer", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "updatePythPullOracle", "accounts": [ @@ -12206,6 +12307,9 @@ }, { "name": "StakeTransfer" + }, + { + "name": "AdminDeposit" } ] } @@ -16218,5 +16322,8 @@ "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" } - ] + ], + "metadata": { + "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" + } } \ No newline at end of file diff --git a/tests/insuranceFundStake.ts b/tests/insuranceFundStake.ts index c90a1795ce..0f67d1814c 100644 --- a/tests/insuranceFundStake.ts +++ b/tests/insuranceFundStake.ts @@ -29,6 +29,7 @@ import { unstakeSharesToAmount, MarketStatus, LIQUIDATION_PCT_PRECISION, + getUserStatsAccountPublicKey, } from '../sdk/src'; import { @@ -40,6 +41,7 @@ import { sleep, mockOracleNoProgram, setFeedPriceNoProgram, + mintUSDCToUser, } from './testHelpers'; import { ContractTier, PERCENTAGE_PRECISION, UserStatus } from '../sdk'; import { startAnchor } from 'solana-bankrun'; @@ -1163,6 +1165,33 @@ describe('insurance fund stake', () => { // assert(usdcBefore.eq(usdcAfter)); }); + it('admin deposit into insurance fund stake', async () => { + await mintUSDCToUser( + usdcMint, + userUSDCAccount.publicKey, + usdcAmount, + bankrunContextWrapper + ); + const marketIndex = 0; + const insuranceFundStakePublicKey = getInsuranceFundStakeAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey + ); + const txSig = await driftClient.depositIntoInsuranceFundStake( + marketIndex, + usdcAmount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userUSDCAccount.publicKey + ); + bankrunContextWrapper.printTxLogs(txSig); + }); + // it('settle spotMarket to insurance vault', async () => { // const marketIndex = new BN(0);