Add force_withdraw state and instruction (#884)

Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
Christian Kamm 2024-02-19 15:06:51 +01:00 committed by GitHub
parent 338a9cb7b8
commit 46c6e86206
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 756 additions and 18 deletions

View File

@ -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"
}
]
}

View File

@ -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;

View File

@ -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>,
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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(())
}

View File

@ -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,

View File

@ -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,

View File

@ -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
///

View File

@ -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,
}

View File

@ -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
}

View File

@ -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.
}

View File

@ -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(())
}

View File

@ -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,

View File

@ -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();

View File

@ -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 = {

View File

@ -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,

View File

@ -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;
}

View File

@ -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"
}
]
}