Add force_withdraw state and instruction (#884)
Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
338a9cb7b8
commit
46c6e86206
|
@ -1067,6 +1067,12 @@
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceWithdrawOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3789,6 +3795,63 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenForceWithdraw",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"vault",
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "ownerAtaTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "alternateOwnerTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Only for the unusual case where the owner_ata account is not owned by account.owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "perpCreateMarket",
|
||||
"docs": [
|
||||
|
@ -7426,12 +7489,16 @@
|
|||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceWithdraw",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -10938,6 +11005,9 @@
|
|||
},
|
||||
{
|
||||
"name": "Serum3PlaceOrderV2"
|
||||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ pub use token_deposit::*;
|
|||
pub use token_deregister::*;
|
||||
pub use token_edit::*;
|
||||
pub use token_force_close_borrows_with_token::*;
|
||||
pub use token_force_withdraw::*;
|
||||
pub use token_liq_bankruptcy::*;
|
||||
pub use token_liq_with_token::*;
|
||||
pub use token_register::*;
|
||||
|
@ -145,6 +146,7 @@ mod token_deposit;
|
|||
mod token_deregister;
|
||||
mod token_edit;
|
||||
mod token_force_close_borrows_with_token;
|
||||
mod token_force_withdraw;
|
||||
mod token_liq_bankruptcy;
|
||||
mod token_liq_with_token;
|
||||
mod token_register;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::associated_token::get_associated_token_address;
|
||||
use anchor_spl::token::Token;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TokenForceWithdraw<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.is_ix_enabled(IxGate::TokenForceWithdraw) @ MangoError::IxIsDisabled,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen,
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccountFixed>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = vault,
|
||||
has_one = oracle,
|
||||
// the mints of bank/vault/token_accounts are implicitly the same because
|
||||
// spl::token::transfer succeeds between token_account and vault
|
||||
)]
|
||||
pub bank: AccountLoader<'info, Bank>,
|
||||
|
||||
#[account(mut)]
|
||||
pub vault: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
/// CHECK: The oracle can be one of several different account types
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
address = get_associated_token_address(&account.load()?.owner, &vault.mint),
|
||||
// NOTE: the owner may have been changed (before immutable owner was a thing)
|
||||
)]
|
||||
pub owner_ata_token_account: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
/// Only for the unusual case where the owner_ata account is not owned by account.owner
|
||||
#[account(
|
||||
mut,
|
||||
constraint = alternate_owner_token_account.owner == account.load()?.owner,
|
||||
)]
|
||||
pub alternate_owner_token_account: Box<Account<'info, TokenAccount>>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
|
@ -95,6 +95,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
|
|||
IxGate::TokenConditionalSwapCreateLinearAuction,
|
||||
);
|
||||
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
|
||||
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
|
||||
|
||||
group.ix_gate = ix_gate;
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ pub use token_deposit::*;
|
|||
pub use token_deregister::*;
|
||||
pub use token_edit::*;
|
||||
pub use token_force_close_borrows_with_token::*;
|
||||
pub use token_force_withdraw::*;
|
||||
pub use token_liq_bankruptcy::*;
|
||||
pub use token_liq_with_token::*;
|
||||
pub use token_register::*;
|
||||
|
@ -127,6 +128,7 @@ mod token_deposit;
|
|||
mod token_deregister;
|
||||
mod token_edit;
|
||||
mod token_force_close_borrows_with_token;
|
||||
mod token_force_withdraw;
|
||||
mod token_liq_bankruptcy;
|
||||
mod token_liq_with_token;
|
||||
mod token_register;
|
||||
|
|
|
@ -55,6 +55,7 @@ pub fn token_edit(
|
|||
platform_liquidation_fee: Option<f32>,
|
||||
disable_asset_liquidation_opt: Option<bool>,
|
||||
collateral_fee_per_day: Option<f32>,
|
||||
force_withdraw_opt: Option<bool>,
|
||||
) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
|
@ -510,6 +511,16 @@ pub fn token_edit(
|
|||
bank.disable_asset_liquidation = u8::from(disable_asset_liquidation);
|
||||
require_group_admin = true;
|
||||
}
|
||||
|
||||
if let Some(force_withdraw) = force_withdraw_opt {
|
||||
msg!(
|
||||
"Force withdraw old {:?}, new {:?}",
|
||||
bank.force_withdraw,
|
||||
force_withdraw
|
||||
);
|
||||
bank.force_withdraw = u8::from(force_withdraw);
|
||||
require_group_admin = true;
|
||||
}
|
||||
}
|
||||
|
||||
// account constraint #1
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{emit_stack, ForceWithdrawLog, TokenBalanceLog};
|
||||
|
||||
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
require!(bank.is_force_withdraw(), MangoError::SomeError);
|
||||
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
|
||||
let withdraw_target = if ctx.accounts.owner_ata_token_account.owner == account.fixed.owner {
|
||||
ctx.accounts.owner_ata_token_account.to_account_info()
|
||||
} else {
|
||||
ctx.accounts.alternate_owner_token_account.to_account_info()
|
||||
};
|
||||
|
||||
let (position, raw_token_index) = account.token_position_mut(token_index)?;
|
||||
let native_position = position.native(&bank);
|
||||
|
||||
// Check >= to allow calling this on 0 deposits to close the token position
|
||||
require_gte!(native_position, I80F48::ZERO);
|
||||
let amount = native_position.floor().to_num::<u64>();
|
||||
let amount_i80f48 = I80F48::from(amount);
|
||||
|
||||
// Update the bank and position
|
||||
let position_is_active = bank.withdraw_without_fee(position, amount_i80f48, now_ts)?;
|
||||
|
||||
// Provide a readable error message in case the vault doesn't have enough tokens
|
||||
if ctx.accounts.vault.amount < amount {
|
||||
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
||||
format!(
|
||||
"bank vault does not have enough tokens, need {} but have {}",
|
||||
amount, ctx.accounts.vault.amount
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer the actual tokens
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
CpiContext::new(
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
token::Transfer {
|
||||
from: ctx.accounts.vault.to_account_info(),
|
||||
to: withdraw_target.clone(),
|
||||
authority: ctx.accounts.group.to_account_info(),
|
||||
},
|
||||
)
|
||||
.with_signer(&[group_seeds]),
|
||||
amount,
|
||||
)?;
|
||||
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
indexed_position: position.indexed_position.to_bits(),
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
// Get the oracle price, even if stale or unconfident: We want to allow force withdraws
|
||||
// even if the oracle is bad.
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
let unsafe_oracle_state = oracle_state_unchecked(
|
||||
&OracleAccountInfos::from_reader(oracle_ref),
|
||||
bank.mint_decimals,
|
||||
)?;
|
||||
|
||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||
let amount_usd = (amount_i80f48 * unsafe_oracle_state.price).to_num::<i64>();
|
||||
account.fixed.net_deposits -= amount_usd;
|
||||
|
||||
if !position_is_active {
|
||||
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
|
||||
}
|
||||
|
||||
emit_stack(ForceWithdrawLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
quantity: amount,
|
||||
price: unsafe_oracle_state.price.to_bits(),
|
||||
to_token_account: withdraw_target.key(),
|
||||
});
|
||||
|
||||
bank.enforce_borrows_lte_deposits()?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -112,6 +112,7 @@ pub fn token_register(
|
|||
reduce_only,
|
||||
force_close: 0,
|
||||
disable_asset_liquidation: u8::from(disable_asset_liquidation),
|
||||
force_withdraw: 0,
|
||||
padding: Default::default(),
|
||||
fees_withdrawn: 0,
|
||||
token_conditional_swap_taker_fee_rate,
|
||||
|
|
|
@ -91,6 +91,7 @@ pub fn token_register_trustless(
|
|||
reduce_only: 2, // deposit-only
|
||||
force_close: 0,
|
||||
disable_asset_liquidation: 1,
|
||||
force_withdraw: 0,
|
||||
padding: Default::default(),
|
||||
fees_withdrawn: 0,
|
||||
token_conditional_swap_taker_fee_rate: 0.0,
|
||||
|
|
|
@ -253,6 +253,7 @@ pub mod mango_v4 {
|
|||
platform_liquidation_fee_opt: Option<f32>,
|
||||
disable_asset_liquidation_opt: Option<bool>,
|
||||
collateral_fee_per_day_opt: Option<f32>,
|
||||
force_withdraw_opt: Option<bool>,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_edit(
|
||||
|
@ -297,6 +298,7 @@ pub mod mango_v4 {
|
|||
platform_liquidation_fee_opt,
|
||||
disable_asset_liquidation_opt,
|
||||
collateral_fee_per_day_opt,
|
||||
force_withdraw_opt,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -817,6 +819,12 @@ pub mod mango_v4 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_force_withdraw(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
/// Perps
|
||||
///
|
||||
|
|
|
@ -788,3 +788,13 @@ pub struct TokenCollateralFeeLog {
|
|||
pub asset_usage_fraction: i128,
|
||||
pub fee: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct ForceWithdrawLog {
|
||||
pub mango_group: Pubkey,
|
||||
pub mango_account: Pubkey,
|
||||
pub token_index: u16,
|
||||
pub quantity: u64,
|
||||
pub price: i128, // I80F48
|
||||
pub to_token_account: Pubkey,
|
||||
}
|
||||
|
|
|
@ -162,8 +162,10 @@ pub struct Bank {
|
|||
/// That means bankrupt accounts may still have assets of this type deposited.
|
||||
pub disable_asset_liquidation: u8,
|
||||
|
||||
pub force_withdraw: u8,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub padding: [u8; 5],
|
||||
pub padding: [u8; 4],
|
||||
|
||||
// Do separate bookkeping for how many tokens were withdrawn
|
||||
// This ensures that collected_fees_native is strictly increasing for stats gathering purposes
|
||||
|
@ -361,7 +363,8 @@ impl Bank {
|
|||
reduce_only: existing_bank.reduce_only,
|
||||
force_close: existing_bank.force_close,
|
||||
disable_asset_liquidation: existing_bank.disable_asset_liquidation,
|
||||
padding: [0; 5],
|
||||
force_withdraw: existing_bank.force_withdraw,
|
||||
padding: [0; 4],
|
||||
token_conditional_swap_taker_fee_rate: existing_bank
|
||||
.token_conditional_swap_taker_fee_rate,
|
||||
token_conditional_swap_maker_fee_rate: existing_bank
|
||||
|
@ -417,6 +420,11 @@ impl Bank {
|
|||
require_eq!(self.maint_asset_weight, I80F48::ZERO);
|
||||
}
|
||||
require_gte!(self.collateral_fee_per_day, 0.0);
|
||||
if self.is_force_withdraw() {
|
||||
require!(self.are_deposits_reduce_only(), MangoError::SomeError);
|
||||
require!(!self.allows_asset_liquidation(), MangoError::SomeError);
|
||||
require_eq!(self.maint_asset_weight, I80F48::ZERO);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -438,6 +446,10 @@ impl Bank {
|
|||
self.force_close == 1
|
||||
}
|
||||
|
||||
pub fn is_force_withdraw(&self) -> bool {
|
||||
self.force_withdraw == 1
|
||||
}
|
||||
|
||||
pub fn allows_asset_liquidation(&self) -> bool {
|
||||
self.disable_asset_liquidation == 0
|
||||
}
|
||||
|
|
|
@ -245,6 +245,7 @@ pub enum IxGate {
|
|||
TokenConditionalSwapCreatePremiumAuction = 69,
|
||||
TokenConditionalSwapCreateLinearAuction = 70,
|
||||
Serum3PlaceOrderV2 = 71,
|
||||
TokenForceWithdraw = 72,
|
||||
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
||||
}
|
||||
|
||||
|
|
|
@ -438,3 +438,113 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_force_withdraw_token() -> Result<(), TransportError> {
|
||||
let test_builder = TestContextBuilder::new();
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..1];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let token = &tokens[0];
|
||||
|
||||
let deposit_amount = 100;
|
||||
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[0],
|
||||
mints,
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: fails when force withdraw isn't enabled
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
TokenForceWithdrawInstruction {
|
||||
account,
|
||||
bank: token.bank,
|
||||
target: context.users[0].token_accounts[0],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// set force withdraw to enabled
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
admin,
|
||||
group,
|
||||
mint: token.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
maint_asset_weight_opt: Some(0.0),
|
||||
reduce_only_opt: Some(1),
|
||||
disable_asset_liquidation_opt: Some(true),
|
||||
force_withdraw_opt: Some(true),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: can't withdraw to foreign address
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
TokenForceWithdrawInstruction {
|
||||
account,
|
||||
bank: token.bank,
|
||||
target: context.users[1].token_accounts[0], // bad address/owner
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
//
|
||||
// TEST: passes and withdraws tokens
|
||||
//
|
||||
let token_account = context.users[0].token_accounts[0];
|
||||
let before_balance = solana.token_account_balance(token_account).await;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenForceWithdrawInstruction {
|
||||
account,
|
||||
bank: token.bank,
|
||||
target: token_account,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let after_balance = solana.token_account_balance(token_account).await;
|
||||
assert_eq!(after_balance, before_balance + deposit_amount);
|
||||
assert!(account_position_closed(solana, account, token.bank).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1328,6 +1328,7 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
|||
platform_liquidation_fee_opt: None,
|
||||
disable_asset_liquidation_opt: None,
|
||||
collateral_fee_per_day_opt: None,
|
||||
force_withdraw_opt: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3112,6 +3113,58 @@ impl ClientInstruction for TokenForceCloseBorrowsWithTokenInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct TokenForceWithdrawInstruction {
|
||||
pub account: Pubkey,
|
||||
pub bank: Pubkey,
|
||||
pub target: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for TokenForceWithdrawInstruction {
|
||||
type Accounts = mango_v4::accounts::TokenForceWithdraw;
|
||||
type Instruction = mango_v4::instruction::TokenForceWithdraw;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
|
||||
let account = account_loader
|
||||
.load_mango_account(&self.account)
|
||||
.await
|
||||
.unwrap();
|
||||
let bank = account_loader.load::<Bank>(&self.bank).await.unwrap();
|
||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
bank: self.bank,
|
||||
vault: bank.vault,
|
||||
oracle: bank.oracle,
|
||||
owner_ata_token_account: self.target,
|
||||
alternate_owner_token_account: self.target,
|
||||
token_program: Token::id(),
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, &instruction);
|
||||
instruction.accounts.extend(health_check_metas.into_iter());
|
||||
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TokenLiqWithTokenInstruction {
|
||||
pub liqee: Pubkey,
|
||||
pub liqor: Pubkey,
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { TokenIndex } from '../src/accounts/bank';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { MANGO_V4_ID } from '../src/constants';
|
||||
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const USER_KEYPAIR =
|
||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const GROUP_PK =
|
||||
process.env.GROUP_PK || '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
|
||||
const TOKEN_INDEX = Number(process.env.TOKEN_INDEX) as TokenIndex;
|
||||
|
||||
async function forceWithdrawTokens(): Promise<void> {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
CLUSTER,
|
||||
MANGO_V4_ID[CLUSTER],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
|
||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
const forceWithdrawBank = group.getFirstBankByTokenIndex(TOKEN_INDEX);
|
||||
if (forceWithdrawBank.reduceOnly != 2) {
|
||||
throw new Error(
|
||||
`Unexpected reduce only state ${forceWithdrawBank.reduceOnly}`,
|
||||
);
|
||||
}
|
||||
if (!forceWithdrawBank.forceWithdraw) {
|
||||
throw new Error(
|
||||
`Unexpected force withdraw state ${forceWithdrawBank.forceWithdraw}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get all mango accounts with deposits for given token
|
||||
const mangoAccountsWithDeposits = (
|
||||
await client.getAllMangoAccounts(group)
|
||||
).filter((a) => a.getTokenBalanceUi(forceWithdrawBank) > 0);
|
||||
|
||||
for (const mangoAccount of mangoAccountsWithDeposits) {
|
||||
const sig = await client.tokenForceWithdraw(
|
||||
group,
|
||||
mangoAccount,
|
||||
TOKEN_INDEX,
|
||||
);
|
||||
console.log(
|
||||
` tokenForceWithdraw for ${mangoAccount.publicKey}, owner ${
|
||||
mangoAccount.owner
|
||||
}, sig https://explorer.solana.com/tx/${sig}?cluster=${
|
||||
CLUSTER == 'devnet' ? 'devnet' : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
forceWithdrawTokens();
|
|
@ -132,6 +132,7 @@ export class Bank implements BankForHealth {
|
|||
reduceOnly: number;
|
||||
forceClose: number;
|
||||
disableAssetLiquidation: number;
|
||||
forceWithdraw: number;
|
||||
feesWithdrawn: BN;
|
||||
tokenConditionalSwapTakerFeeRate: number;
|
||||
tokenConditionalSwapMakerFeeRate: number;
|
||||
|
@ -218,6 +219,7 @@ export class Bank implements BankForHealth {
|
|||
obj.disableAssetLiquidation == 0,
|
||||
obj.collectedCollateralFees,
|
||||
obj.collateralFeePerDay,
|
||||
obj.forceWithdraw == 1,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -286,6 +288,7 @@ export class Bank implements BankForHealth {
|
|||
public allowAssetLiquidation: boolean,
|
||||
collectedCollateralFees: I80F48Dto,
|
||||
public collateralFeePerDay: number,
|
||||
public forceWithdraw: boolean,
|
||||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
this.oracleConfig = {
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import {
|
||||
AnchorProvider,
|
||||
BN,
|
||||
Instruction,
|
||||
Program,
|
||||
Provider,
|
||||
Wallet,
|
||||
} from '@coral-xyz/anchor';
|
||||
import * as borsh from '@coral-xyz/borsh';
|
||||
import { AnchorProvider, BN, Program, Wallet } from '@coral-xyz/anchor';
|
||||
import { OpenOrders, decodeEventQueue } from '@project-serum/serum';
|
||||
import {
|
||||
createAccount,
|
||||
createCloseAccountInstruction,
|
||||
createInitializeAccount3Instruction,
|
||||
unpackAccount,
|
||||
} from '@solana/spl-token';
|
||||
import {
|
||||
AccountInfo,
|
||||
|
@ -26,13 +20,13 @@ import {
|
|||
RecentPrioritizationFees,
|
||||
SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
Signer,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
TransactionSignature,
|
||||
} from '@solana/web3.js';
|
||||
import bs58 from 'bs58';
|
||||
import chunk from 'lodash/chunk';
|
||||
import copy from 'fast-copy';
|
||||
import chunk from 'lodash/chunk';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
|
@ -45,7 +39,6 @@ import {
|
|||
Serum3Orders,
|
||||
TokenConditionalSwap,
|
||||
TokenConditionalSwapDisplayPriceStyle,
|
||||
TokenConditionalSwapDto,
|
||||
TokenConditionalSwapIntention,
|
||||
TokenPosition,
|
||||
} from './accounts/mangoAccount';
|
||||
|
@ -70,7 +63,6 @@ import {
|
|||
} from './accounts/serum3';
|
||||
import {
|
||||
IxGateParams,
|
||||
PerpEditParams,
|
||||
TokenEditParams,
|
||||
TokenRegisterParams,
|
||||
buildIxGate,
|
||||
|
@ -559,6 +551,7 @@ export class MangoClient {
|
|||
params.platformLiquidationFee,
|
||||
params.disableAssetLiquidation,
|
||||
params.collateralFeePerDay,
|
||||
params.forceWithdraw,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -625,6 +618,94 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async tokenForceWithdraw(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
tokenIndex: TokenIndex,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const bank = group.getFirstBankByTokenIndex(tokenIndex);
|
||||
if (!bank.forceWithdraw) {
|
||||
throw new Error('Bank is not in force-withdraw mode');
|
||||
}
|
||||
|
||||
const ownerAtaTokenAccount = await getAssociatedTokenAddress(
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
true,
|
||||
);
|
||||
let alternateOwnerTokenAccount = PublicKey.default;
|
||||
const preInstructions: TransactionInstruction[] = [];
|
||||
const postInstructions: TransactionInstruction[] = [];
|
||||
|
||||
const ai = await this.connection.getAccountInfo(ownerAtaTokenAccount);
|
||||
|
||||
// ensure withdraws don't fail with missing ATAs
|
||||
if (ai == null) {
|
||||
preInstructions.push(
|
||||
await createAssociatedTokenAccountIdempotentInstruction(
|
||||
(this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
mangoAccount.owner,
|
||||
bank.mint,
|
||||
),
|
||||
);
|
||||
|
||||
// wsol case
|
||||
if (bank.mint.equals(NATIVE_MINT)) {
|
||||
postInstructions.push(
|
||||
createCloseAccountInstruction(
|
||||
ownerAtaTokenAccount,
|
||||
mangoAccount.owner,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const account = await unpackAccount(ownerAtaTokenAccount, ai);
|
||||
// if owner is not same as mango account's owner on the ATA (for whatever reason)
|
||||
// then create another token account
|
||||
if (!account.owner.equals(mangoAccount.owner)) {
|
||||
const kp = Keypair.generate();
|
||||
alternateOwnerTokenAccount = kp.publicKey;
|
||||
await createAccount(
|
||||
this.connection,
|
||||
(this.program.provider as AnchorProvider).wallet as any as Signer,
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
kp,
|
||||
);
|
||||
|
||||
// wsol case
|
||||
if (bank.mint.equals(NATIVE_MINT)) {
|
||||
postInstructions.push(
|
||||
createCloseAccountInstruction(
|
||||
alternateOwnerTokenAccount,
|
||||
mangoAccount.owner,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ix = await this.program.methods
|
||||
.tokenForceWithdraw()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
oracle: bank.oracle,
|
||||
ownerAtaTokenAccount,
|
||||
alternateOwnerTokenAccount,
|
||||
})
|
||||
.instruction();
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [
|
||||
...preInstructions,
|
||||
ix,
|
||||
...postInstructions,
|
||||
]);
|
||||
}
|
||||
|
||||
public async tokenDeregister(
|
||||
group: Group,
|
||||
mintPk: PublicKey,
|
||||
|
|
|
@ -117,6 +117,7 @@ export interface TokenEditParams {
|
|||
platformLiquidationFee: number | null;
|
||||
disableAssetLiquidation: boolean | null;
|
||||
collateralFeePerDay: number | null;
|
||||
forceWithdraw: boolean | null;
|
||||
}
|
||||
|
||||
export const NullTokenEditParams: TokenEditParams = {
|
||||
|
@ -160,6 +161,7 @@ export const NullTokenEditParams: TokenEditParams = {
|
|||
platformLiquidationFee: null,
|
||||
disableAssetLiquidation: null,
|
||||
collateralFeePerDay: null,
|
||||
forceWithdraw: null,
|
||||
};
|
||||
|
||||
export interface PerpEditParams {
|
||||
|
@ -307,6 +309,7 @@ export interface IxGateParams {
|
|||
TokenConditionalSwapCreatePremiumAuction: boolean;
|
||||
TokenConditionalSwapCreateLinearAuction: boolean;
|
||||
Serum3PlaceOrderV2: boolean;
|
||||
TokenForceWithdraw: boolean;
|
||||
}
|
||||
|
||||
// Default with all ixs enabled, use with buildIxGate
|
||||
|
@ -386,6 +389,7 @@ export const TrueIxGateParams: IxGateParams = {
|
|||
TokenConditionalSwapCreatePremiumAuction: true,
|
||||
TokenConditionalSwapCreateLinearAuction: true,
|
||||
Serum3PlaceOrderV2: true,
|
||||
TokenForceWithdraw: true,
|
||||
};
|
||||
|
||||
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
||||
|
@ -475,6 +479,7 @@ export function buildIxGate(p: IxGateParams): BN {
|
|||
toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69);
|
||||
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
||||
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
||||
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
|
||||
|
||||
return ixGate;
|
||||
}
|
||||
|
|
|
@ -1067,6 +1067,12 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceWithdrawOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3789,6 +3795,63 @@ export type MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenForceWithdraw",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"vault",
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "ownerAtaTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "alternateOwnerTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Only for the unusual case where the owner_ata account is not owned by account.owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "perpCreateMarket",
|
||||
"docs": [
|
||||
|
@ -7426,12 +7489,16 @@ export type MangoV4 = {
|
|||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceWithdraw",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -10938,6 +11005,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Serum3PlaceOrderV2"
|
||||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -15245,6 +15315,12 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceWithdrawOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -17967,6 +18043,63 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenForceWithdraw",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bank",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"vault",
|
||||
"oracle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "ownerAtaTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "alternateOwnerTokenAccount",
|
||||
"isMut": true,
|
||||
"isSigner": false,
|
||||
"docs": [
|
||||
"Only for the unusual case where the owner_ata account is not owned by account.owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tokenProgram",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "perpCreateMarket",
|
||||
"docs": [
|
||||
|
@ -21604,12 +21737,16 @@ export const IDL: MangoV4 = {
|
|||
],
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceWithdraw",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -25116,6 +25253,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Serum3PlaceOrderV2"
|
||||
},
|
||||
{
|
||||
"name": "TokenForceWithdraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue