force close tokens program part (#518)

* force close tokens

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* add test

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* reset

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

---------

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2023-04-13 12:44:12 +02:00 committed by GitHub
parent 2dca3003df
commit b07857c696
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 800 additions and 27 deletions

View File

@ -715,7 +715,7 @@
{
"name": "reduceOnlyOpt",
"type": {
"option": "bool"
"option": "u8"
}
},
{
@ -723,6 +723,12 @@
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -2570,6 +2576,45 @@
}
]
},
{
"name": "tokenForceCloseBorrowsWithToken",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "liqor",
"isMut": true,
"isSigner": false
},
{
"name": "liqorOwner",
"isMut": false,
"isSigner": true
},
{
"name": "liqee",
"isMut": true,
"isSigner": false
}
],
"args": [
{
"name": "assetTokenIndex",
"type": "u16"
},
{
"name": "liabTokenIndex",
"type": "u16"
},
{
"name": "maxLiabTransfer",
"type": "u64"
}
]
},
{
"name": "tokenLiqBankruptcy",
"accounts": [
@ -4125,12 +4170,16 @@
"name": "reduceOnly",
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
2119
2118
]
}
}
@ -6981,6 +7030,9 @@
},
{
"name": "AccountBuybackFeesWithMngo"
},
{
"name": "TokenForceCloseBorrowsWithToken"
}
]
}
@ -8684,6 +8736,11 @@
"code": 6045,
"name": "HealthRegionBadInnerInstruction",
"msg": "HealthRegions allow only specific instructions between Begin and End"
},
{
"code": 6046,
"name": "TokenInForceClose",
"msg": "token is in force close"
}
]
}

View File

@ -47,6 +47,7 @@ pub use token_add_bank::*;
pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
pub use token_force_close_borrows_with_token::*;
pub use token_liq_bankruptcy::*;
pub use token_liq_with_token::*;
pub use token_register::*;
@ -103,6 +104,7 @@ mod token_add_bank;
mod token_deposit;
mod token_deregister;
mod token_edit;
mod token_force_close_borrows_with_token;
mod token_liq_bankruptcy;
mod token_liq_with_token;
mod token_register;

View File

@ -0,0 +1,27 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct TokenForceCloseBorrowsWithToken<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::TokenForceCloseBorrowsWithToken) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = liqor.load()?.is_operational() @ MangoError::AccountIsFrozen
// liqor_owner is checked at #1
)]
pub liqor: AccountLoader<'info, MangoAccountFixed>,
pub liqor_owner: Signer<'info>,
#[account(
mut,
has_one = group,
constraint = liqee.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub liqee: AccountLoader<'info, MangoAccountFixed>,
}

View File

@ -97,6 +97,8 @@ pub enum MangoError {
PerpOrderIdNotFound,
#[msg("HealthRegions allow only specific instructions between Begin and End")]
HealthRegionBadInnerInstruction,
#[msg("token is in force close")]
TokenInForceClose,
}
impl MangoError {

View File

@ -343,10 +343,15 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
let change_amount = change.amount - loan_origination_fee;
let native_after_change = native + change_amount;
if bank.is_reduce_only() {
if bank.are_deposits_reduce_only() {
require!(
(change_amount < 0 && native_after_change >= 0)
|| (change_amount > 0 && native_after_change < 1),
native_after_change < 1 || native_after_change <= native,
MangoError::TokenInReduceOnlyMode
);
}
if bank.are_borrows_reduce_only() {
require!(
native_after_change >= native || native_after_change >= 0,
MangoError::TokenInReduceOnlyMode
);
}

View File

@ -65,6 +65,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
log_if_changed(&group, ix_gate, IxGate::TokenUpdateIndexAndRate);
log_if_changed(&group, ix_gate, IxGate::TokenWithdraw);
log_if_changed(&group, ix_gate, IxGate::AccountBuybackFeesWithMngo);
log_if_changed(&group, ix_gate, IxGate::TokenForceCloseBorrowsWithToken);
group.ix_gate = ix_gate;

View File

@ -47,6 +47,7 @@ pub use token_add_bank::*;
pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
pub use token_force_close_borrows_with_token::*;
pub use token_liq_bankruptcy::*;
pub use token_liq_with_token::*;
pub use token_register::*;
@ -103,6 +104,7 @@ mod token_add_bank;
mod token_deposit;
mod token_deregister;
mod token_edit;
mod token_force_close_borrows_with_token;
mod token_liq_bankruptcy;
mod token_liq_with_token;
mod token_register;

View File

@ -51,7 +51,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
let account = self.account.load_full()?;
let position = account.token_position(token_index)?;
let amount_i80f48 = if reduce_only || bank.is_reduce_only() {
let amount_i80f48 = if reduce_only || bank.are_deposits_reduce_only() {
position
.native(&bank)
.min(I80F48::ZERO)
@ -61,7 +61,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
} else {
I80F48::from(amount)
};
if bank.is_reduce_only() {
if bank.are_deposits_reduce_only() {
require!(
reduce_only || amount_i80f48 == I80F48::from(amount),
MangoError::TokenInReduceOnlyMode

View File

@ -36,8 +36,9 @@ pub fn token_edit(
deposit_weight_scale_start_quote_opt: Option<f64>,
reset_stable_price: bool,
reset_net_borrow_limit: bool,
reduce_only_opt: Option<bool>,
reduce_only_opt: Option<u8>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -274,14 +275,15 @@ pub fn token_edit(
msg!(
"Reduce only: old - {:?}, new - {:?}",
bank.reduce_only,
u8::from(reduce_only)
reduce_only
);
bank.reduce_only = u8::from(reduce_only);
// security admin can only enable reduce_only
if !reduce_only {
// security admin can only make it stricter
// anything that makes it less strict, should require admin
if reduce_only == 0 || (reduce_only == 2 && bank.reduce_only == 1) {
require_group_admin = true;
}
bank.reduce_only = reduce_only;
};
if let Some(name) = name_opt.as_ref() {
@ -289,6 +291,19 @@ pub fn token_edit(
bank.name = fill_from_str(&name)?;
require_group_admin = true;
};
if let Some(force_close) = force_close_opt {
if force_close {
require!(bank.reduce_only > 0, MangoError::SomeError);
}
msg!(
"Force close: old - {:?}, new - {:?}",
bank.force_close,
u8::from(force_close)
);
bank.force_close = u8::from(force_close);
require_group_admin = true;
};
}
// account constraint #1

View File

@ -0,0 +1,202 @@
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::logs::TokenBalanceLog;
use crate::state::*;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
pub fn token_force_close_borrows_with_token(
ctx: Context<TokenForceCloseBorrowsWithToken>,
// which asset tokens are allowed, is checked at #3
asset_token_index: TokenIndex,
// token's force_close flag is checked at #2
liab_token_index: TokenIndex,
max_liab_transfer: u64,
) -> Result<()> {
let group_pk = &ctx.accounts.group.key();
require_neq!(asset_token_index, liab_token_index, MangoError::SomeError);
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever")?;
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
// account constraint #1
require!(
liqor
.fixed
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require_msg_typed!(
!liqor.fixed.being_liquidated(),
MangoError::BeingLiquidated,
"liqor account"
);
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
//
// Transfer liab_token from liqor to liqee to close the borrows.
// Transfer corresponding amount of asset_token from liqee to liqor.
//
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
{
let liqor: &mut MangoAccountRefMut = &mut liqor.borrow_mut();
let liqor_key = ctx.accounts.liqor.key();
let liqee: &mut MangoAccountRefMut = &mut liqee.borrow_mut();
let liqee_key = ctx.accounts.liqee.key();
let (asset_bank, asset_oracle_price, opt_liab_bank_and_price) =
account_retriever.banks_mut_and_oracles(asset_token_index, liab_token_index)?;
let (liab_bank, liab_oracle_price) = opt_liab_bank_and_price.unwrap();
// account constraint #2
require!(liab_bank.is_force_close(), MangoError::TokenInForceClose);
// account constraint #3
// only allow combination of asset and liab token,
// where liqee's health would be guaranteed to not decrease
require_gte!(
liab_bank.init_liab_weight,
asset_bank.init_liab_weight * (I80F48::ONE + liab_bank.liquidation_fee),
MangoError::SomeError
);
let (liqee_asset_position, liqee_asset_raw_index) =
liqee.token_position_and_raw_index(asset_token_index)?;
let liqee_asset_native = liqee_asset_position.native(asset_bank);
let (liqee_liab_position, liqee_liab_raw_index, _) =
liqee.ensure_token_position(liab_token_index)?;
let liqee_liab_native = liqee_liab_position.native(liab_bank);
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
let (liqor_liab_position, liqor_liab_raw_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let liqor_liab_native = liqor_liab_position.native(liab_bank);
// Require that liqor obtain deposits before he tries to liquidate liqee's borrows, to prevent
// moving other liquidator from earning further liquidation fee from these borrows by trying to liquidate the current liqor
require!(liqor_liab_native.is_positive(), MangoError::SomeError);
// The amount of liab native tokens we will transfer
let max_liab_transfer = I80F48::from(max_liab_transfer);
let liab_transfer = max_liab_transfer
.min(-liqee_liab_native)
.min(liqor_liab_native)
.max(I80F48::ZERO);
// The amount of asset native tokens we will give up for them
let fee_factor = I80F48::ONE + liab_bank.liquidation_fee;
let liab_oracle_price_adjusted = liab_oracle_price * fee_factor;
let asset_transfer = liab_transfer * liab_oracle_price_adjusted / asset_oracle_price;
// Apply the balance changes to the liqor and liqee accounts
let liqee_liab_active =
liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer, now_ts)?;
let liqee_liab_indexed_position = liqee_liab_position.indexed_position;
let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(
liqor_liab_position,
liab_transfer,
now_ts,
liab_oracle_price,
)?;
let liqor_liab_indexed_position = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
let liqor_asset_active =
asset_bank.deposit(liqor_asset_position, asset_transfer, now_ts)?;
let liqor_asset_indexed_position = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index);
let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(
liqee_asset_position,
asset_transfer,
now_ts,
asset_oracle_price,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
msg!(
"Force closed {} liab for {} asset",
liab_transfer,
asset_transfer
);
// liqee asset
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqee_key,
token_index: asset_token_index,
indexed_position: liqee_asset_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
});
// liqee liab
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqee_key,
token_index: liab_token_index,
indexed_position: liqee_liab_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
});
// liqor asset
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: asset_token_index,
indexed_position: liqor_asset_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
});
// liqor liab
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: liab_token_index,
indexed_position: liqor_liab_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
});
let liqee_health_cache = new_health_cache(&liqee.borrow(), &mut account_retriever)
.context("create liqee health cache")?;
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
// Since we use a scanning account retriever, it's safe to deactivate inactive token positions
if !liqee_asset_active {
liqee.deactivate_token_position_and_log(liqee_asset_raw_index, liqee_key);
}
if !liqee_liab_active {
liqee.deactivate_token_position_and_log(liqee_liab_raw_index, liqee_key);
}
if !liqor_asset_active {
liqor.deactivate_token_position_and_log(liqor_asset_raw_index, liqor_key);
}
if !liqor_liab_active {
liqor.deactivate_token_position_and_log(liqor_liab_raw_index, liqor_key)
}
};
// Check liqor's health
// This should always improve liqor health, since we decrease the zero-asset-weight
// liab token and gain some asset token, this check is just for denfensive measure
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &mut account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
// TODO log
// emit!(TokenForceCloseBorrowWithToken
Ok(())
}

View File

@ -88,7 +88,8 @@ pub fn token_register(
borrow_weight_scale_start_quote: f64::MAX,
deposit_weight_scale_start_quote: f64::MAX,
reduce_only: 0,
reserved: [0; 2119],
force_close: 0,
reserved: [0; 2118],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);

View File

@ -74,7 +74,8 @@ pub fn token_register_trustless(
borrow_weight_scale_start_quote: 100_000_000_000.0, // $100k
deposit_weight_scale_start_quote: 100_000_000_000.0, // $100k
reduce_only: 0,
reserved: [0; 2119],
force_close: 0,
reserved: [0; 2118],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);

View File

@ -52,7 +52,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let is_borrow = amount > native_position;
require!(allow_borrow || !is_borrow, MangoError::SomeError);
if bank.is_reduce_only() {
if bank.are_borrows_reduce_only() {
require!(!is_borrow, MangoError::TokenInReduceOnlyMode);
}

View File

@ -169,8 +169,9 @@ pub mod mango_v4 {
deposit_weight_scale_start_quote_opt: Option<f64>,
reset_stable_price: bool,
reset_net_borrow_limit: bool,
reduce_only_opt: Option<bool>,
reduce_only_opt: Option<u8>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_edit(
@ -198,6 +199,7 @@ pub mod mango_v4 {
reset_net_borrow_limit,
reduce_only_opt,
name_opt,
force_close_opt,
)?;
Ok(())
}
@ -540,6 +542,22 @@ pub mod mango_v4 {
Ok(())
}
pub fn token_force_close_borrows_with_token(
ctx: Context<TokenForceCloseBorrowsWithToken>,
asset_token_index: TokenIndex,
liab_token_index: TokenIndex,
max_liab_transfer: u64,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_force_close_borrows_with_token(
ctx,
asset_token_index,
liab_token_index,
max_liab_transfer,
)?;
Ok(())
}
pub fn token_liq_bankruptcy(
ctx: Context<TokenLiqBankruptcy>,
max_liab_transfer: I80F48,

View File

@ -127,10 +127,16 @@ pub struct Bank {
/// See scaled_init_asset_weight().
pub deposit_weight_scale_start_quote: f64,
// We have 3 modes
// 0 - Off,
// 1 - ReduceDepositsReduceBorrows - standard
// 2 - ReduceBorrows - borrows can only be reduced, but deposits have no restriction, special case for
// force close mode, where liqor should first acquire deposits before closing liqee's borrows
pub reduce_only: u8,
pub force_close: u8,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 2119],
pub reserved: [u8; 2118],
}
const_assert_eq!(
size_of::<Bank>(),
@ -158,7 +164,8 @@ const_assert_eq!(
+ 8
+ 8
+ 1
+ 2119
+ 1
+ 2118
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -219,7 +226,8 @@ impl Bank {
borrow_weight_scale_start_quote: f64::MAX,
deposit_weight_scale_start_quote: f64::MAX,
reduce_only: 0,
reserved: [0; 2119],
force_close: 0,
reserved: [0; 2118],
}
}
@ -229,10 +237,18 @@ impl Bank {
.trim_matches(char::from(0))
}
pub fn is_reduce_only(&self) -> bool {
pub fn are_deposits_reduce_only(&self) -> bool {
self.reduce_only == 1
}
pub fn are_borrows_reduce_only(&self) -> bool {
self.reduce_only == 1 || self.reduce_only == 2
}
pub fn is_force_close(&self) -> bool {
self.force_close == 1
}
#[inline(always)]
pub fn native_borrows(&self) -> I80F48 {
self.borrow_index * self.indexed_borrows

View File

@ -187,6 +187,7 @@ pub enum IxGate {
TokenUpdateIndexAndRate = 46,
TokenWithdraw = 47,
AccountBuybackFeesWithMngo = 48,
TokenForceCloseBorrowsWithToken = 49,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -18,6 +18,7 @@ mod test_benchmark;
mod test_borrow_limits;
mod test_delegate;
mod test_fees_buyback_with_mngo;
mod test_force_close;
mod test_health_compute;
mod test_health_region;
mod test_ix_gate_set;

View File

@ -0,0 +1,230 @@
use super::*;
#[tokio::test]
async fn test_force_close() -> 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..4];
let payer_mint_accounts = &context.users[1].token_accounts[0..4];
//
// 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 collateral_token = &tokens[0];
let borrow_token = &tokens[1];
send_tx(
solana,
TokenEditWeights {
group,
admin,
mint: mints[1].pubkey,
init_asset_weight: 0.6,
maint_asset_weight: 0.8,
maint_liab_weight: 1.2,
init_liab_weight: 1.5, // changed from 1.4
},
)
.await
.unwrap();
// deposit some funds, to the vaults aren't empty
create_funded_account(
&solana,
group,
owner,
99,
&context.users[1],
mints,
100000,
0,
)
.await;
let deposit1_amount = 100;
let liqor = create_funded_account(
&solana,
group,
owner,
1,
&context.users[0],
&[mints[0]],
deposit1_amount,
0,
)
.await;
//
// SETUP: Make an account with some collateral and some borrows
//
let liqee = create_funded_account(
&solana,
group,
owner,
0,
&context.users[0],
&[mints[0]],
deposit1_amount,
0,
)
.await;
let borrow1_amount = 10;
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow1_amount,
allow_borrow: true,
account: liqee,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
//
// test force close is enabled
//
assert!(send_tx(
solana,
TokenForceCloseBorrowsWithTokenInstruction {
liqee: liqee,
liqor: liqor,
liqor_owner: owner,
asset_token_index: collateral_token.index,
liab_token_index: borrow_token.index,
max_liab_transfer: 10000,
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
.is_err());
// set force close, and reduce only to 1
send_tx(
solana,
TokenMakeReduceOnly {
admin,
group,
mint: mints[1].pubkey,
reduce_only: 1,
force_close: false,
},
)
.await
.unwrap();
//
// test liqor needs deposits to be gte than the borrows it wants to liquidate
//
assert!(send_tx(
solana,
TokenForceCloseBorrowsWithTokenInstruction {
liqee: liqee,
liqor: liqor,
liqor_owner: owner,
asset_token_index: collateral_token.index,
liab_token_index: borrow_token.index,
max_liab_transfer: 10000,
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
.is_err());
//
// test deposit with reduce only set to 1
//
let deposit1_amount = 11;
assert!(send_tx(
solana,
TokenDepositInstruction {
amount: deposit1_amount,
reduce_only: false,
account: liqor,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.is_err());
// set force close, and reduce only to 2
send_tx(
solana,
TokenMakeReduceOnly {
admin,
group,
mint: mints[1].pubkey,
reduce_only: 2,
force_close: true,
},
)
.await
.unwrap();
//
// test deposit with reduce only set to 2
//
send_tx(
solana,
TokenDepositInstruction {
amount: deposit1_amount,
reduce_only: false,
account: liqor,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// test force close borrows
//
send_tx(
solana,
TokenForceCloseBorrowsWithTokenInstruction {
liqee: liqee,
liqor: liqor,
liqor_owner: owner,
asset_token_index: collateral_token.index,
liab_token_index: borrow_token.index,
max_liab_transfer: 10000,
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
.unwrap();
assert!(account_position_closed(solana, liqee, borrow_token.bank).await);
assert_eq!(
account_position(solana, liqee, collateral_token.bank).await,
100 - 10
);
Ok(())
}

View File

@ -60,6 +60,8 @@ async fn test_reduce_only_token() -> Result<(), TransportError> {
admin,
group,
mint: mints[0].pubkey,
reduce_only: 1,
force_close: false,
},
)
.await
@ -159,6 +161,8 @@ async fn test_reduce_only_token() -> Result<(), TransportError> {
admin,
group,
mint: mints[2].pubkey,
reduce_only: 1,
force_close: false,
},
)
.await

View File

@ -1072,6 +1072,7 @@ fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
reset_net_borrow_limit: false,
reduce_only_opt: None,
name_opt: None,
force_close_opt: None,
}
}
@ -1259,6 +1260,8 @@ pub struct TokenMakeReduceOnly {
pub group: Pubkey,
pub admin: TestKeypair,
pub mint: Pubkey,
pub reduce_only: u8,
pub force_close: bool,
}
#[async_trait::async_trait(?Send)]
@ -1283,7 +1286,8 @@ impl ClientInstruction for TokenMakeReduceOnly {
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
let instruction = Self::Instruction {
reduce_only_opt: Some(true),
reduce_only_opt: Some(self.reduce_only),
force_close_opt: Some(self.force_close),
..token_edit_instruction_default()
};
@ -2595,6 +2599,69 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction {
}
}
pub struct TokenForceCloseBorrowsWithTokenInstruction {
pub liqee: Pubkey,
pub liqor: Pubkey,
pub liqor_owner: TestKeypair,
pub asset_token_index: TokenIndex,
pub asset_bank_index: usize,
pub liab_token_index: TokenIndex,
pub liab_bank_index: usize,
pub max_liab_transfer: u64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenForceCloseBorrowsWithTokenInstruction {
type Accounts = mango_v4::accounts::TokenForceCloseBorrowsWithToken;
type Instruction = mango_v4::instruction::TokenForceCloseBorrowsWithToken;
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 {
asset_token_index: self.asset_token_index,
liab_token_index: self.liab_token_index,
max_liab_transfer: self.max_liab_transfer,
};
let liqee = account_loader
.load_mango_account(&self.liqee)
.await
.unwrap();
let liqor = account_loader
.load_mango_account(&self.liqor)
.await
.unwrap();
let health_check_metas = derive_liquidation_remaining_account_metas(
&account_loader,
&liqee,
&liqor,
self.asset_token_index,
self.asset_bank_index,
self.liab_token_index,
self.liab_bank_index,
)
.await;
let accounts = Self::Accounts {
group: liqee.fixed.group,
liqee: self.liqee,
liqor: self.liqor,
liqor_owner: self.liqor_owner.pubkey(),
};
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![self.liqor_owner]
}
}
pub struct TokenLiqWithTokenInstruction {
pub liqee: Pubkey,
pub liqor: Pubkey,

View File

@ -139,6 +139,7 @@ async function tokenEdit(): Promise<void> {
params.resetNetBorrowLimit ?? false,
params.reduceOnly,
params.name,
params.forceClose,
)
.accounts({
group: group.publicKey,

View File

@ -8,6 +8,7 @@ import {
AddressLookupTableAccount,
Cluster,
Commitment,
Connection,
Keypair,
MemcmpFilter,
PublicKey,
@ -17,7 +18,6 @@ import {
SystemProgram,
TransactionInstruction,
TransactionSignature,
Connection,
} from '@solana/web3.js';
import bs58 from 'bs58';
import { Bank, MintInfo, TokenIndex } from './accounts/bank';
@ -398,6 +398,7 @@ export class MangoClient {
params.resetNetBorrowLimit ?? false,
params.reduceOnly,
params.name,
params.forceClose,
)
.accounts({
group: group.publicKey,

View File

@ -24,8 +24,9 @@ export interface TokenEditParams {
depositWeightScaleStartQuote: number | null;
resetStablePrice: boolean | null;
resetNetBorrowLimit: boolean | null;
reduceOnly: boolean | null;
reduceOnly: number | null;
name: string | null;
forceClose: boolean | null;
}
export const NullTokenEditParams: TokenEditParams = {
@ -52,6 +53,7 @@ export const NullTokenEditParams: TokenEditParams = {
resetNetBorrowLimit: null,
reduceOnly: null,
name: null,
forceClose: null,
};
export interface PerpEditParams {
@ -172,6 +174,7 @@ export interface IxGateParams {
TokenUpdateIndexAndRate: boolean;
TokenWithdraw: boolean;
AccountBuybackFeesWithMngo: boolean;
TokenForceCloseBorrowsWithToken: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -228,6 +231,7 @@ export const TrueIxGateParams: IxGateParams = {
TokenUpdateIndexAndRate: true,
TokenWithdraw: true,
AccountBuybackFeesWithMngo: true,
TokenForceCloseBorrowsWithToken: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -294,6 +298,7 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenUpdateIndexAndRate', 46);
toggleIx(ixGate, p, 'TokenWithdraw', 47);
toggleIx(ixGate, p, 'AccountBuybackFeesWithMngo', 48);
toggleIx(ixGate, p, 'TokenForceCloseBorrowsWithToken', 49);
return ixGate;
}

View File

@ -715,7 +715,7 @@ export type MangoV4 = {
{
"name": "reduceOnlyOpt",
"type": {
"option": "bool"
"option": "u8"
}
},
{
@ -723,6 +723,12 @@ export type MangoV4 = {
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -2570,6 +2576,45 @@ export type MangoV4 = {
}
]
},
{
"name": "tokenForceCloseBorrowsWithToken",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "liqor",
"isMut": true,
"isSigner": false
},
{
"name": "liqorOwner",
"isMut": false,
"isSigner": true
},
{
"name": "liqee",
"isMut": true,
"isSigner": false
}
],
"args": [
{
"name": "assetTokenIndex",
"type": "u16"
},
{
"name": "liabTokenIndex",
"type": "u16"
},
{
"name": "maxLiabTransfer",
"type": "u64"
}
]
},
{
"name": "tokenLiqBankruptcy",
"accounts": [
@ -4125,12 +4170,16 @@ export type MangoV4 = {
"name": "reduceOnly",
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
2119
2118
]
}
}
@ -6981,6 +7030,9 @@ export type MangoV4 = {
},
{
"name": "AccountBuybackFeesWithMngo"
},
{
"name": "TokenForceCloseBorrowsWithToken"
}
]
}
@ -8684,6 +8736,11 @@ export type MangoV4 = {
"code": 6045,
"name": "HealthRegionBadInnerInstruction",
"msg": "HealthRegions allow only specific instructions between Begin and End"
},
{
"code": 6046,
"name": "TokenInForceClose",
"msg": "token is in force close"
}
]
};
@ -9405,7 +9462,7 @@ export const IDL: MangoV4 = {
{
"name": "reduceOnlyOpt",
"type": {
"option": "bool"
"option": "u8"
}
},
{
@ -9413,6 +9470,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -11260,6 +11323,45 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "tokenForceCloseBorrowsWithToken",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "liqor",
"isMut": true,
"isSigner": false
},
{
"name": "liqorOwner",
"isMut": false,
"isSigner": true
},
{
"name": "liqee",
"isMut": true,
"isSigner": false
}
],
"args": [
{
"name": "assetTokenIndex",
"type": "u16"
},
{
"name": "liabTokenIndex",
"type": "u16"
},
{
"name": "maxLiabTransfer",
"type": "u64"
}
]
},
{
"name": "tokenLiqBankruptcy",
"accounts": [
@ -12815,12 +12917,16 @@ export const IDL: MangoV4 = {
"name": "reduceOnly",
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
2119
2118
]
}
}
@ -15671,6 +15777,9 @@ export const IDL: MangoV4 = {
},
{
"name": "AccountBuybackFeesWithMngo"
},
{
"name": "TokenForceCloseBorrowsWithToken"
}
]
}
@ -17374,6 +17483,11 @@ export const IDL: MangoV4 = {
"code": 6045,
"name": "HealthRegionBadInnerInstruction",
"msg": "HealthRegions allow only specific instructions between Begin and End"
},
{
"code": 6046,
"name": "TokenInForceClose",
"msg": "token is in force close"
}
]
};