liq: functionality fixes and test

This commit is contained in:
Christian Kamm 2022-03-29 11:49:26 +02:00
parent 69426d6d96
commit d6ebffd346
8 changed files with 335 additions and 21 deletions

1
Cargo.lock generated
View File

@ -1566,6 +1566,7 @@ dependencies = [
"env_logger",
"fixed",
"fixed-macro",
"itertools 0.10.3",
"log",
"mango-macro",
"margin-trade",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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