diff --git a/programs/drift_vaults/src/instructions/constraints.rs b/programs/drift_vaults/src/instructions/constraints.rs index ada9cb32..e1b53900 100644 --- a/programs/drift_vaults/src/instructions/constraints.rs +++ b/programs/drift_vaults/src/instructions/constraints.rs @@ -2,6 +2,13 @@ use crate::{Vault, VaultDepositor}; use anchor_lang::prelude::*; +pub fn is_vault_for_vault_depositor( + vault_depositor: &AccountLoader, + vault: &AccountLoader, +) -> anchor_lang::Result { + Ok(vault_depositor.load()?.vault.eq(&vault.key())) +} + pub fn is_authority_for_vault_depositor( vault_depositor: &AccountLoader, signer: &Signer, diff --git a/programs/drift_vaults/src/instructions/force_withdraw.rs b/programs/drift_vaults/src/instructions/force_withdraw.rs new file mode 100644 index 00000000..11e6904f --- /dev/null +++ b/programs/drift_vaults/src/instructions/force_withdraw.rs @@ -0,0 +1,141 @@ +use crate::constraints::*; +use crate::cpi::{TokenTransferCPI, WithdrawCPI}; +use crate::{declare_vault_seeds, AccountMapProvider}; +use crate::{Vault, VaultDepositor}; +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Transfer}; +use anchor_spl::token::{Token, TokenAccount}; +use drift::cpi::accounts::Withdraw as DriftWithdraw; +use drift::instructions::optional_accounts::AccountMaps; +use drift::program::Drift; +use drift::state::user::User; + +pub fn force_withdraw<'info>(ctx: Context<'_, '_, '_, 'info, ForceWithdraw<'info>>) -> Result<()> { + let clock = &Clock::get()?; + let mut vault = ctx.accounts.vault.load_mut()?; + let mut vault_depositor = ctx.accounts.vault_depositor.load_mut()?; + + let user = ctx.accounts.drift_user.load()?; + let spot_market_index = vault.spot_market_index; + + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = ctx.load_maps(clock.slot, Some(spot_market_index))?; + + let vault_equity = + vault.calculate_equity(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; + + let (withdraw_amount, _) = + vault_depositor.withdraw(vault_equity, &mut vault, clock.unix_timestamp)?; + + msg!("force_withdraw_amount: {}", withdraw_amount); + + drop(vault); + drop(user); + + ctx.drift_withdraw(withdraw_amount)?; + + ctx.token_transfer(withdraw_amount)?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct ForceWithdraw<'info> { + #[account( + mut, + constraint = is_manager_for_vault(&vault, &manager)? + )] + pub vault: AccountLoader<'info, Vault>, + pub manager: Signer<'info>, + #[account( + mut, + constraint = is_vault_for_vault_depositor(&vault_depositor, &vault)?, + )] + pub vault_depositor: AccountLoader<'info, VaultDepositor>, + pub authority: Signer<'info>, + #[account( + mut, + seeds = [b"vault_token_account".as_ref(), vault.key().as_ref()], + bump, + )] + pub vault_token_account: Box>, + #[account( + mut, + constraint = is_user_stats_for_vault(&vault, &drift_user_stats)? + )] + /// CHECK: checked in drift cpi + pub drift_user_stats: AccountInfo<'info>, + #[account( + mut, + constraint = is_user_for_vault(&vault, &drift_user.key())? + )] + /// CHECK: checked in drift cpi + pub drift_user: AccountLoader<'info, User>, + /// CHECK: checked in drift cpi + pub drift_state: AccountInfo<'info>, + #[account( + mut, + token::mint = vault_token_account.mint + )] + pub drift_spot_market_vault: Box>, + /// CHECK: checked in drift cpi + pub drift_signer: AccountInfo<'info>, + #[account( + mut, + token::authority = vault_depositor.load()?.authority, + token::mint = vault_token_account.mint + )] + pub user_token_account: Box>, + pub drift_program: Program<'info, Drift>, + pub token_program: Program<'info, Token>, +} + +impl<'info> WithdrawCPI for Context<'_, '_, '_, 'info, ForceWithdraw<'info>> { + fn drift_withdraw(&self, amount: u64) -> Result<()> { + declare_vault_seeds!(self.accounts.vault, seeds); + let spot_market_index = self.accounts.vault.load()?.spot_market_index; + + let cpi_accounts = DriftWithdraw { + state: self.accounts.drift_state.to_account_info().clone(), + user: self.accounts.drift_user.to_account_info().clone(), + user_stats: self.accounts.drift_user_stats.to_account_info().clone(), + authority: self.accounts.vault.to_account_info().clone(), + spot_market_vault: self + .accounts + .drift_spot_market_vault + .to_account_info() + .clone(), + drift_signer: self.accounts.drift_signer.to_account_info().clone(), + user_token_account: self.accounts.vault_token_account.to_account_info().clone(), + token_program: self.accounts.token_program.to_account_info().clone(), + }; + + let drift_program = self.accounts.drift_program.to_account_info().clone(); + let cpi_context = CpiContext::new_with_signer(drift_program, cpi_accounts, seeds) + .with_remaining_accounts(self.remaining_accounts.into()); + drift::cpi::withdraw(cpi_context, spot_market_index, amount, false)?; + + Ok(()) + } +} + +impl<'info> TokenTransferCPI for Context<'_, '_, '_, 'info, ForceWithdraw<'info>> { + fn token_transfer(&self, amount: u64) -> Result<()> { + declare_vault_seeds!(self.accounts.vault, seeds); + + let cpi_accounts = Transfer { + from: self.accounts.vault_token_account.to_account_info().clone(), + to: self.accounts.user_token_account.to_account_info().clone(), + authority: self.accounts.vault.to_account_info().clone(), + }; + let token_program = self.accounts.token_program.to_account_info().clone(); + let cpi_context = CpiContext::new_with_signer(token_program, cpi_accounts, seeds); + + token::transfer(cpi_context, amount)?; + + Ok(()) + } +} diff --git a/programs/drift_vaults/src/instructions/mod.rs b/programs/drift_vaults/src/instructions/mod.rs index 95fa0086..71485905 100644 --- a/programs/drift_vaults/src/instructions/mod.rs +++ b/programs/drift_vaults/src/instructions/mod.rs @@ -1,5 +1,6 @@ pub use cancel_withdraw_request::*; pub use deposit::*; +pub use force_withdraw::*; pub use initialize_vault::*; pub use initialize_vault_depositor::*; pub use liquidate::*; @@ -14,6 +15,7 @@ pub use withdraw::*; mod cancel_withdraw_request; pub mod constraints; mod deposit; +mod force_withdraw; mod initialize_vault; mod initialize_vault_depositor; mod liquidate; diff --git a/programs/drift_vaults/src/lib.rs b/programs/drift_vaults/src/lib.rs index a3d643a1..ac66b365 100644 --- a/programs/drift_vaults/src/lib.rs +++ b/programs/drift_vaults/src/lib.rs @@ -66,6 +66,12 @@ pub mod drift_vaults { instructions::withdraw(ctx) } + pub fn force_withdraw<'info>( + ctx: Context<'_, '_, '_, 'info, ForceWithdraw<'info>>, + ) -> Result<()> { + instructions::force_withdraw(ctx) + } + pub fn liquidate<'info>(ctx: Context<'_, '_, '_, 'info, Liquidate<'info>>) -> Result<()> { instructions::liquidate(ctx) } diff --git a/ts/sdk/src/types/drift_vaults.ts b/ts/sdk/src/types/drift_vaults.ts index e1762246..626b52ae 100644 --- a/ts/sdk/src/types/drift_vaults.ts +++ b/ts/sdk/src/types/drift_vaults.ts @@ -385,6 +385,77 @@ export type DriftVaults = { ]; args: []; }, + { + name: 'forceWithdraw'; + accounts: [ + { + name: 'vault'; + isMut: true; + isSigner: false; + }, + { + name: 'manager'; + isMut: false; + isSigner: true; + }, + { + name: 'vaultDepositor'; + isMut: true; + isSigner: false; + }, + { + name: 'authority'; + isMut: false; + isSigner: true; + }, + { + name: 'vaultTokenAccount'; + isMut: true; + isSigner: false; + }, + { + name: 'driftUserStats'; + isMut: true; + isSigner: false; + }, + { + name: 'driftUser'; + isMut: true; + isSigner: false; + }, + { + name: 'driftState'; + isMut: false; + isSigner: false; + }, + { + name: 'driftSpotMarketVault'; + isMut: true; + isSigner: false; + }, + { + name: 'driftSigner'; + isMut: false; + isSigner: false; + }, + { + name: 'userTokenAccount'; + isMut: true; + isSigner: false; + }, + { + name: 'driftProgram'; + isMut: false; + isSigner: false; + }, + { + name: 'tokenProgram'; + isMut: false; + isSigner: false; + } + ]; + args: []; + }, { name: 'liquidate'; accounts: [ @@ -1208,6 +1279,11 @@ export type DriftVaults = { code: 6017; name: 'PermissionedVault'; msg: 'PermissionedVault'; + }, + { + code: 6018; + name: 'WithdrawInProgress'; + msg: 'WithdrawInProgress'; } ]; }; @@ -1599,6 +1675,77 @@ export const IDL: DriftVaults = { ], args: [], }, + { + name: 'forceWithdraw', + accounts: [ + { + name: 'vault', + isMut: true, + isSigner: false, + }, + { + name: 'manager', + isMut: false, + isSigner: true, + }, + { + name: 'vaultDepositor', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'vaultTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'driftUserStats', + isMut: true, + isSigner: false, + }, + { + name: 'driftUser', + isMut: true, + isSigner: false, + }, + { + name: 'driftState', + isMut: false, + isSigner: false, + }, + { + name: 'driftSpotMarketVault', + isMut: true, + isSigner: false, + }, + { + name: 'driftSigner', + isMut: false, + isSigner: false, + }, + { + name: 'userTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'driftProgram', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + ], + args: [], + }, { name: 'liquidate', accounts: [ @@ -2423,5 +2570,10 @@ export const IDL: DriftVaults = { name: 'PermissionedVault', msg: 'PermissionedVault', }, + { + code: 6018, + name: 'WithdrawInProgress', + msg: 'WithdrawInProgress', + }, ], };