liq: functionality fixes and test
This commit is contained in:
parent
69426d6d96
commit
d6ebffd346
|
@ -1566,6 +1566,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"fixed",
|
||||
"fixed-macro",
|
||||
"itertools 0.10.3",
|
||||
"log",
|
||||
"mango-macro",
|
||||
"margin-trade",
|
||||
|
|
|
@ -35,7 +35,7 @@ checked_math = { path = "../../lib/checked_math" }
|
|||
arrayref = "0.3.6"
|
||||
num_enum = "0.5.1"
|
||||
bincode = "1.3.3"
|
||||
mango-macro={ path = "../../mango-macro" }
|
||||
mango-macro = { path = "../../mango-macro" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-sdk = { version = "1.9.5", default-features = false }
|
||||
|
@ -49,3 +49,4 @@ env_logger = "0.9.0"
|
|||
base64 = "0.13.0"
|
||||
async-trait = "0.1.52"
|
||||
margin-trade = { path = "../margin-trade", features = ["cpi"] }
|
||||
itertools = "0.10.3"
|
||||
|
|
|
@ -14,9 +14,9 @@ pub struct LiqTokenWithToken<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = liqor.load()?.owner == liqor_owner.key(),
|
||||
)]
|
||||
pub liqor: AccountLoader<'info, MangoAccount>,
|
||||
#[account(address = liqor.load()?.owner)]
|
||||
pub liqor_owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -40,14 +40,20 @@ pub fn liq_token_with_token(
|
|||
let mut liqor = ctx.accounts.liqor.load_mut()?;
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
|
||||
//
|
||||
// Health computation
|
||||
//
|
||||
// Initial liqee health check
|
||||
let mut liqee_health_cache = health_cache_for_liqee(&liqee, &account_retriever)?;
|
||||
let init_health = liqee_health_cache.health(HealthType::Init)?;
|
||||
msg!("pre liqee health: {}", init_health);
|
||||
// TODO: actual check involving being_liquidated and maint_health
|
||||
require!(init_health < 0, MangoError::SomeError);
|
||||
if liqee.being_liquidated {
|
||||
if init_health > I80F48::ZERO {
|
||||
liqee.being_liquidated = false;
|
||||
msg!("Liqee init_health above zero");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint)?;
|
||||
require!(maint_health < I80F48::ZERO, MangoError::SomeError);
|
||||
liqee.being_liquidated = true;
|
||||
}
|
||||
|
||||
//
|
||||
// Transfer some liab_token from liqor to liqee and
|
||||
|
@ -108,7 +114,7 @@ pub fn liq_token_with_token(
|
|||
);
|
||||
|
||||
// The amount of asset native tokens we will give up for them
|
||||
let asset_transfer = cm!(liab_transfer * asset_price / liab_price_adjusted);
|
||||
let asset_transfer = cm!(liab_transfer * liab_price_adjusted / asset_price);
|
||||
|
||||
// Apply the balance changes to the liqor and liqee accounts
|
||||
liab_bank.deposit(
|
||||
|
@ -138,17 +144,29 @@ pub fn liq_token_with_token(
|
|||
// Update the health cache
|
||||
liqee_health_cache.adjust_token_balance(liab_token_index, liab_transfer)?;
|
||||
liqee_health_cache.adjust_token_balance(asset_token_index, -asset_transfer)?;
|
||||
|
||||
msg!(
|
||||
"liquidated {} liab for {} asset",
|
||||
liab_transfer,
|
||||
asset_transfer
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Check liqee's health again: bankrupt? no longer being_liquidated?
|
||||
// Check liqee health again
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint)?;
|
||||
let init_health = liqee_health_cache.health(HealthType::Init)?;
|
||||
msg!("post liqee health: {} {}", maint_health, init_health);
|
||||
if maint_health < I80F48::ZERO {
|
||||
// TODO: bankruptcy check?
|
||||
} else {
|
||||
let init_health = liqee_health_cache.health(HealthType::Init)?;
|
||||
|
||||
// TODO: Check liqor's health
|
||||
// this is equivalent to one native USDC or 1e-6 USDC
|
||||
// This is used as threshold to flip flag instead of 0 because of dust issues
|
||||
liqee.being_liquidated = init_health < -I80F48::ONE;
|
||||
}
|
||||
|
||||
// Check liqor's health
|
||||
let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever)?;
|
||||
msg!("post liqor health: {}", liqor_health);
|
||||
require!(liqor_health > 0, MangoError::SomeError);
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
// TOOD: this must deactivate token accounts if the deposit/withdraw calls above call for it
|
||||
|
||||
|
|
|
@ -127,6 +127,20 @@ pub mod mango_v4 {
|
|||
instructions::serum3_liq_force_cancel_orders(ctx, limit)
|
||||
}
|
||||
|
||||
pub fn liq_token_with_token(
|
||||
ctx: Context<LiqTokenWithToken>,
|
||||
asset_token_index: TokenIndex,
|
||||
liab_token_index: TokenIndex,
|
||||
max_liab_transfer: I80F48,
|
||||
) -> Result<()> {
|
||||
instructions::liq_token_with_token(
|
||||
ctx,
|
||||
asset_token_index,
|
||||
liab_token_index,
|
||||
max_liab_transfer,
|
||||
)
|
||||
}
|
||||
|
||||
///
|
||||
/// Perps
|
||||
///
|
||||
|
|
|
@ -244,7 +244,7 @@ impl HealthCache {
|
|||
.iter_mut()
|
||||
.find(|t| t.token_index == token_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?;
|
||||
entry.balance = cm!(entry.balance + change);
|
||||
entry.balance = cm!(entry.balance + change * entry.oracle_price);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use anchor_lang::prelude::*;
|
|||
use anchor_lang::solana_program::sysvar::{self, SysvarId};
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
use fixed::types::I80F48;
|
||||
use itertools::Itertools;
|
||||
use solana_program::instruction::Instruction;
|
||||
use solana_sdk::instruction;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
|
@ -160,6 +161,62 @@ async fn derive_health_check_remaining_account_metas(
|
|||
.collect()
|
||||
}
|
||||
|
||||
async fn derive_liquidation_remaining_account_metas(
|
||||
account_loader: &impl ClientAccountLoader,
|
||||
liqee: &MangoAccount,
|
||||
liqor: &MangoAccount,
|
||||
asset_token_index: TokenIndex,
|
||||
liab_token_index: TokenIndex,
|
||||
) -> Vec<AccountMeta> {
|
||||
let mut banks = vec![];
|
||||
let mut oracles = vec![];
|
||||
let token_indexes = liqee
|
||||
.token_account_map
|
||||
.iter_active()
|
||||
.chain(liqor.token_account_map.iter_active())
|
||||
.map(|ta| ta.token_index)
|
||||
.unique();
|
||||
for token_index in token_indexes {
|
||||
let mint_info = get_mint_info_by_token_index(account_loader, liqee, token_index).await;
|
||||
let lookup_table = account_loader
|
||||
.load_bytes(&mint_info.address_lookup_table)
|
||||
.await
|
||||
.unwrap();
|
||||
let addresses = mango_v4::address_lookup_table::addresses(&lookup_table);
|
||||
let writable_bank = token_index == asset_token_index || token_index == liab_token_index;
|
||||
banks.push((
|
||||
addresses[mint_info.address_lookup_table_bank_index as usize],
|
||||
writable_bank,
|
||||
));
|
||||
oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
|
||||
}
|
||||
|
||||
let serum_oos = liqee
|
||||
.serum3_account_map
|
||||
.iter_active()
|
||||
.chain(liqor.serum3_account_map.iter_active())
|
||||
.map(|&s| s.open_orders);
|
||||
|
||||
banks
|
||||
.iter()
|
||||
.map(|(pubkey, is_writable)| AccountMeta {
|
||||
pubkey: *pubkey,
|
||||
is_writable: *is_writable,
|
||||
is_signer: false,
|
||||
})
|
||||
.chain(oracles.iter().map(|&pubkey| AccountMeta {
|
||||
pubkey,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
}))
|
||||
.chain(serum_oos.map(|pubkey| AccountMeta {
|
||||
pubkey,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_serum_style_pubkey(d: &[u64; 4]) -> Pubkey {
|
||||
Pubkey::new(bytemuck::cast_slice(d as &[_]))
|
||||
}
|
||||
|
@ -1082,6 +1139,59 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct LiqTokenWithTokenInstruction<'keypair> {
|
||||
pub liqee: Pubkey,
|
||||
pub liqor: Pubkey,
|
||||
pub liqor_owner: &'keypair Keypair,
|
||||
|
||||
pub asset_token_index: TokenIndex,
|
||||
pub liab_token_index: TokenIndex,
|
||||
pub max_liab_transfer: I80F48,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::LiqTokenWithToken;
|
||||
type Instruction = mango_v4::instruction::LiqTokenWithToken;
|
||||
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: MangoAccount = account_loader.load(&self.liqee).await.unwrap();
|
||||
let liqor: MangoAccount = account_loader.load(&self.liqor).await.unwrap();
|
||||
let health_check_metas = derive_liquidation_remaining_account_metas(
|
||||
&account_loader,
|
||||
&liqee,
|
||||
&liqor,
|
||||
self.asset_token_index,
|
||||
self.liab_token_index,
|
||||
)
|
||||
.await;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: liqee.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<&Keypair> {
|
||||
vec![self.liqor_owner]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreatePerpMarketInstruction<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
|
|
|
@ -68,11 +68,11 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
RegisterTokenInstruction {
|
||||
token_index,
|
||||
decimals: mint.decimals,
|
||||
maint_asset_weight: 0.9,
|
||||
init_asset_weight: 0.8,
|
||||
maint_liab_weight: 1.1,
|
||||
init_liab_weight: 1.2,
|
||||
liquidation_fee: 0.0,
|
||||
maint_asset_weight: 0.8,
|
||||
init_asset_weight: 0.6,
|
||||
maint_liab_weight: 1.2,
|
||||
init_liab_weight: 1.4,
|
||||
liquidation_fee: 0.02,
|
||||
group,
|
||||
admin,
|
||||
mint: mint.pubkey,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
use mango_v4::state::*;
|
||||
use program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
@ -207,3 +209,171 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = &Keypair::new();
|
||||
let owner = &context.users[0].key;
|
||||
let payer = &context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..2];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let base_token = &tokens[0];
|
||||
let quote_token = &tokens[1];
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
account_num: 2,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
DepositInstruction {
|
||||
amount: 10000,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
//
|
||||
// SETUP: Make an account and deposit some quote, borrow some base
|
||||
//
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
let deposit_amount = 1000;
|
||||
send_tx(
|
||||
solana,
|
||||
DepositInstruction {
|
||||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let withdraw_amount = 400;
|
||||
send_tx(
|
||||
solana,
|
||||
WithdrawInstruction {
|
||||
amount: withdraw_amount,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Change the oracle to make health go negative, then liquidate
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
SetStubOracle {
|
||||
mint: base_token.mint.pubkey,
|
||||
payer,
|
||||
price: "2.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// first limited liq
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: quote_token.index,
|
||||
liab_token_index: base_token.index,
|
||||
max_liab_transfer: I80F48::from_num(10.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// the asset cost for 10 liab is 10 * 2 * 1.04 = 20.8
|
||||
assert_eq!(
|
||||
account_position(solana, account, base_token.bank).await,
|
||||
-400 + 10
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, quote_token.bank).await,
|
||||
1000 - 21
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated);
|
||||
|
||||
// remaining liq
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: quote_token.index,
|
||||
liab_token_index: base_token.index,
|
||||
max_liab_transfer: I80F48::from_num(10000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// health before liquidation was 1000 * 0.6 - 400 * 1.4 = -520
|
||||
// liab needed 520 / (1.4*2 - 0.6*2*(1 + 0.02 + 0.02)) = 335.0515
|
||||
// asset cost = 335 * 2 * 1.04 = 696.8
|
||||
assert_eq!(
|
||||
account_position(solana, account, base_token.bank).await,
|
||||
-400 + 335
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, quote_token.bank).await,
|
||||
1000 - 697
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(!liqee.being_liquidated);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue