Merge pull request #95 from blockworks-foundation/ckamm/insurance-fund

liq_token_bankruptcy for socialized loss and insurance fund
This commit is contained in:
Christian Kamm 2022-07-06 11:47:28 +02:00 committed by GitHub
commit 5b72e85634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1843 additions and 356 deletions

View File

@ -78,12 +78,12 @@ impl MangoClient {
// Mango Account
let mut mango_account_tuples = program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 40,
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 72,
offset: 40,
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
encoding: None,
}),
@ -135,12 +135,12 @@ impl MangoClient {
}
let mango_account_tuples = program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 40,
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 72,
offset: 40,
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
encoding: None,
}),
@ -155,7 +155,7 @@ impl MangoClient {
let mut banks_cache = HashMap::new();
let mut banks_cache_by_token_index = HashMap::new();
let bank_tuples = program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
@ -197,7 +197,7 @@ impl MangoClient {
let mut serum3_external_markets_cache = HashMap::new();
let serum3_market_tuples =
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
@ -221,7 +221,7 @@ impl MangoClient {
let mut perp_markets_cache_by_perp_market_index = HashMap::new();
let perp_market_tuples =
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 24,
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
@ -279,12 +279,12 @@ impl MangoClient {
pub fn get_account(&self) -> Result<(Pubkey, MangoAccount), anchor_client::ClientError> {
let mango_accounts = self.program().accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 40,
offset: 8,
bytes: MemcmpEncodedBytes::Base58(self.group().to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 72,
offset: 40,
bytes: MemcmpEncodedBytes::Base58(self.payer().to_string()),
encoding: None,
}),

View File

@ -1,6 +1,7 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Token;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
@ -33,9 +34,9 @@ pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
}
let account = ctx.accounts.account.load()?;
require_eq!(account.being_liquidated, 0);
require!(!account.being_liquidated(), MangoError::SomeError);
require!(!account.is_bankrupt(), MangoError::SomeError);
require_eq!(account.delegate, Pubkey::default());
require_eq!(account.is_bankrupt, 0);
for ele in account.tokens.values {
require_eq!(ele.is_active(), false);
}

View File

@ -37,8 +37,8 @@ pub fn create_account(ctx: Context<CreateAccount>, account_num: u8, name: String
account.tokens = MangoAccountTokenPositions::default();
account.serum3 = MangoAccountSerum3Orders::default();
account.perps = MangoAccountPerpPositions::default();
account.being_liquidated = 0;
account.is_bankrupt = 0;
account.set_being_liquidated(false);
account.set_bankrupt(false);
Ok(())
}

View File

@ -1,4 +1,5 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Mint, Token, TokenAccount};
use crate::error::*;
use crate::state::*;
@ -17,15 +18,31 @@ pub struct CreateGroup<'info> {
pub admin: Signer<'info>,
pub insurance_mint: Account<'info, Mint>,
#[account(
init,
seeds = [group.key().as_ref(), b"InsuranceVault".as_ref()],
bump,
token::authority = group,
token::mint = insurance_mint,
payer = payer
)]
pub insurance_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
pub fn create_group(ctx: Context<CreateGroup>, group_num: u32, testing: u8) -> Result<()> {
let mut group = ctx.accounts.group.load_init()?;
group.admin = ctx.accounts.admin.key();
group.insurance_vault = ctx.accounts.insurance_vault.key();
group.insurance_mint = ctx.accounts.insurance_mint.key();
group.bump = *ctx.bumps.get("group").ok_or(MangoError::SomeError)?;
group.group_num = group_num;
group.testing = testing;

View File

@ -85,7 +85,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>(
let group = ctx.accounts.group.load()?;
let mut account = ctx.accounts.account.load_mut()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Go over the banks passed as health accounts and:
// - Ensure that all banks that are passed in have activated positions.

View File

@ -162,7 +162,7 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>(
let group_seeds = group_seeds!(group);
let mut account = ctx.accounts.account.load_mut()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Find index at which vaults start
let vaults_index = ctx

View File

@ -167,7 +167,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>,
) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Find index at which vaults start
let vaults_index = ctx

View File

@ -0,0 +1,218 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use anchor_spl::token::Token;
use anchor_spl::token::TokenAccount;
use fixed::types::I80F48;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::ScanningAccountRetriever;
use crate::state::*;
use crate::util::checked_math as cm;
// Remaining accounts:
// - all banks for liab_token_index (writable)
// - merged health accounts for liqor+liqee
#[derive(Accounts)]
#[instruction(liab_token_index: TokenIndex)]
pub struct LiqTokenBankruptcy<'info> {
#[account(
has_one = insurance_vault,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()),
)]
pub liqor: AccountLoader<'info, MangoAccount>,
pub liqor_owner: Signer<'info>,
#[account(
mut,
has_one = group,
)]
pub liqee: AccountLoader<'info, MangoAccount>,
#[account(
has_one = group,
constraint = liab_mint_info.load()?.token_index == liab_token_index,
)]
pub liab_mint_info: AccountLoader<'info, MintInfo>,
#[account(mut)]
pub quote_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub insurance_vault: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
impl<'info> LiqTokenBankruptcy<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.insurance_vault.to_account_info(),
to: self.quote_vault.to_account_info(),
authority: self.group.to_account_info(),
};
CpiContext::new(program, accounts)
}
}
pub fn liq_token_bankruptcy(
ctx: Context<LiqTokenBankruptcy>,
liab_token_index: TokenIndex,
max_liab_transfer: I80F48,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
let group_pk = &ctx.accounts.group.key();
// split remaining accounts into banks and health
let liab_mint_info = ctx.accounts.liab_mint_info.load()?;
let bank_pks = liab_mint_info.banks();
let (bank_ais, health_ais) = &ctx.remaining_accounts.split_at(bank_pks.len());
require!(
bank_ais.iter().map(|ai| ai.key).eq(bank_pks.iter()),
MangoError::SomeError
);
let mut liqor = ctx.accounts.liqor.load_mut()?;
require!(!liqor.is_bankrupt(), MangoError::IsBankrupt);
let mut liqee = ctx.accounts.liqee.load_mut()?;
require!(liqee.is_bankrupt(), MangoError::SomeError);
let liab_bank = bank_ais[0].load::<Bank>()?;
let liab_deposit_index = liab_bank.deposit_index;
let (liqee_liab, liqee_raw_token_index, _) =
liqee.tokens.get_mut_or_create(liab_token_index)?;
let mut remaining_liab_loss = -liqee_liab.native(&liab_bank);
require_gt!(remaining_liab_loss, I80F48::ZERO);
drop(liab_bank);
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
// find insurance transfer amount
let (liab_bank, liab_price, opt_quote_bank_and_price) =
account_retriever.banks_mut_and_oracles(liab_token_index, QUOTE_TOKEN_INDEX)?;
let liab_fee_factor = if liab_token_index == QUOTE_TOKEN_INDEX {
I80F48::ONE
} else {
cm!(I80F48::ONE + liab_bank.liquidation_fee)
};
let liab_price_adjusted = cm!(liab_price * liab_fee_factor);
let liab_transfer_unrounded = remaining_liab_loss.min(max_liab_transfer);
let insurance_transfer = cm!(liab_transfer_unrounded * liab_price_adjusted)
.checked_ceil()
.unwrap()
.checked_to_num::<u64>()
.unwrap()
.min(ctx.accounts.insurance_vault.amount);
let insurance_fund_exhausted = insurance_transfer == ctx.accounts.insurance_vault.amount;
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
// AUDIT: v3 does this, but it seems bad, because it can make liab_transfer
// exceed max_liab_transfer due to the ceil() above! Otoh, not doing it would allow
// liquidators to exploit the insurance fund for 1 native token each call.
let liab_transfer = cm!(insurance_transfer_i80f48 / liab_price_adjusted);
let mut liqee_liab_active = true;
if insurance_transfer > 0 {
// in the end, the liqee gets liab assets
liqee_liab_active = liab_bank.deposit(liqee_liab, liab_transfer)?;
remaining_liab_loss = -liqee_liab.native(&liab_bank);
// move insurance assets into quote bank
let group_seeds = group_seeds!(group);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
insurance_transfer,
)?;
// move quote assets into liqor and withdraw liab assets
if let Some((quote_bank, _)) = opt_quote_bank_and_price {
require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key());
require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint);
// credit the liqor
let (liqor_quote, liqor_quote_raw_token_index, _) =
liqor.tokens.get_mut_or_create(QUOTE_TOKEN_INDEX)?;
let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?;
// transfer liab from liqee to liqor
let (liqor_liab, liqor_liab_raw_token_index, _) =
liqor.tokens.get_mut_or_create(liab_token_index)?;
let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?;
// Check liqor's health
let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever)?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
if !liqor_quote_active {
liqor.tokens.deactivate(liqor_quote_raw_token_index);
}
if !liqor_liab_active {
liqor.tokens.deactivate(liqor_liab_raw_token_index);
}
} else {
// For liab_token_index == QUOTE_TOKEN_INDEX: the insurance fund deposits directly into liqee,
// without a fee or the liqor being involved
require_eq!(liab_token_index, QUOTE_TOKEN_INDEX);
require_eq!(liab_price_adjusted, I80F48::ONE);
require_eq!(insurance_transfer_i80f48, liab_transfer);
}
}
drop(account_retriever);
// Socialize loss
if insurance_fund_exhausted && remaining_liab_loss.is_positive() {
// find the total deposits
let mut indexed_total_deposits = I80F48::ZERO;
for bank_ai in bank_ais.iter() {
let bank = bank_ai.load::<Bank>()?;
indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits);
}
// This is the solution to:
// total_indexed_deposits * (deposit_index - new_deposit_index) = remaining_liab_loss
// AUDIT: Could it happen that remaining_liab_loss > total_indexed_deposits * deposit_index?
// Probably not.
let new_deposit_index =
cm!(liab_deposit_index - remaining_liab_loss / indexed_total_deposits);
let mut amount_to_credit = remaining_liab_loss;
let mut position_active = true;
for bank_ai in bank_ais.iter() {
let mut bank = bank_ai.load_mut::<Bank>()?;
bank.deposit_index = new_deposit_index;
// credit liqee on each bank where we can offset borrows
let amount_for_bank = amount_to_credit.min(bank.native_borrows());
if amount_for_bank.is_positive() {
position_active = bank.deposit(liqee_liab, amount_for_bank)?;
amount_to_credit = cm!(amount_to_credit - amount_for_bank);
if amount_to_credit.is_zero() {
break;
}
}
}
require!(!position_active, MangoError::SomeError);
liqee_liab_active = false;
}
// If the account has no more borrows then it's no longer bankrupt
let account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
let liqee_health_cache = new_health_cache(&liqee, &account_retriever)?;
liqee.set_bankrupt(liqee_health_cache.has_borrows());
if !liqee_liab_active {
liqee.tokens.deactivate(liqee_raw_token_index);
}
Ok(())
}

View File

@ -15,7 +15,7 @@ pub struct LiqTokenWithToken<'info> {
#[account(
mut,
has_one = group,
constraint = liqor.load()?.owner == liqor_owner.key() || liqor.load()?.delegate == liqor_owner.key(),
constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()),
)]
pub liqor: AccountLoader<'info, MangoAccount>,
pub liqor_owner: Signer<'info>,
@ -39,24 +39,24 @@ pub fn liq_token_with_token(
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?;
let mut liqor = ctx.accounts.liqor.load_mut()?;
require!(liqor.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!liqor.is_bankrupt(), MangoError::IsBankrupt);
let mut liqee = ctx.accounts.liqee.load_mut()?;
require!(liqee.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!liqee.is_bankrupt(), MangoError::IsBankrupt);
// Initial liqee health check
let mut liqee_health_cache = new_health_cache(&liqee, &account_retriever)?;
let init_health = liqee_health_cache.health(HealthType::Init)?;
if liqee.being_liquidated != 0 {
if liqee.being_liquidated() {
if init_health > I80F48::ZERO {
liqee.being_liquidated = 0;
liqee.set_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 = 1;
liqee.set_being_liquidated(true);
}
//
@ -68,8 +68,9 @@ pub fn liq_token_with_token(
//
// This must happen _after_ the health computation, since immutable borrows of
// the bank are not allowed at the same time.
let (asset_bank, liab_bank, asset_price, liab_price) =
let (asset_bank, asset_price, opt_liab_bank_and_price) =
account_retriever.banks_mut_and_oracles(asset_token_index, liab_token_index)?;
let (liab_bank, liab_price) = opt_liab_bank_and_price.unwrap();
let liqee_assets_native = liqee
.tokens
@ -207,13 +208,13 @@ pub fn liq_token_with_token(
// Check liqee health again
let maint_health = liqee_health_cache.health(HealthType::Maint)?;
if maint_health < I80F48::ZERO {
// TODO: bankruptcy check?
liqee.set_bankrupt(!liqee_health_cache.has_liquidatable_assets());
} else {
let init_health = liqee_health_cache.health(HealthType::Init)?;
// 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 = if init_health < -I80F48::ONE { 1 } else { 0 };
liqee.set_being_liquidated(init_health < -I80F48::ONE);
}
// Check liqor's health

View File

@ -10,6 +10,7 @@ pub use edit_account::*;
pub use flash_loan::*;
pub use flash_loan2::*;
pub use flash_loan3::*;
pub use liq_token_bankruptcy::*;
pub use liq_token_with_token::*;
pub use perp_cancel_all_orders::*;
pub use perp_cancel_all_orders_by_side::*;
@ -51,6 +52,7 @@ mod edit_account;
mod flash_loan;
mod flash_loan2;
mod flash_loan3;
mod liq_token_bankruptcy;
mod liq_token_with_token;
mod perp_cancel_all_orders;
mod perp_cancel_all_orders_by_side;

View File

@ -30,7 +30,7 @@ pub struct PerpCancelAllOrders<'info> {
pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> Result<()> {
let mut mango_account = ctx.accounts.account.load_mut()?;
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;

View File

@ -34,7 +34,7 @@ pub fn perp_cancel_all_orders_by_side(
limit: u8,
) -> Result<()> {
let mut mango_account = ctx.accounts.account.load_mut()?;
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;

View File

@ -30,7 +30,7 @@ pub struct PerpCancelOrder<'info> {
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Result<()> {
let mut mango_account = ctx.accounts.account.load_mut()?;
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
let perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;

View File

@ -33,7 +33,7 @@ pub fn perp_cancel_order_by_client_order_id(
client_order_id: u64,
) -> Result<()> {
let mut mango_account = ctx.accounts.account.load_mut()?;
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
let perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;

View File

@ -50,7 +50,6 @@ pub fn perp_create_market(
oracle_config: OracleConfig,
base_token_index_opt: Option<TokenIndex>,
base_token_decimals: u8,
quote_token_index: TokenIndex,
quote_lot_size: i64,
base_lot_size: i64,
maint_asset_weight: f32,
@ -96,7 +95,8 @@ pub fn perp_create_market(
base_token_decimals,
perp_market_index,
base_token_index: base_token_index_opt.ok_or(TokenIndex::MAX).unwrap(),
quote_token_index,
padding: Default::default(),
reserved: Default::default(),
};
let mut bids = ctx.accounts.bids.load_init()?;

View File

@ -79,7 +79,7 @@ pub fn perp_place_order(
limit: u8,
) -> Result<()> {
let mut mango_account = ctx.accounts.account.load_mut()?;
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
let mango_account_pk = ctx.accounts.account.key();
{

View File

@ -50,7 +50,7 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
//
{
let account = ctx.accounts.account.load()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
let serum_market = ctx.accounts.serum_market.load()?;

View File

@ -63,7 +63,7 @@ pub fn serum3_cancel_order(
//
{
let account = ctx.accounts.account.load()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Validate open_orders
require!(

View File

@ -41,7 +41,7 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
//
let mut account = ctx.accounts.account.load_mut()?;
let serum_market = ctx.accounts.serum_market.load()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Validate open_orders
require!(
account

View File

@ -51,7 +51,7 @@ pub fn serum3_create_open_orders(ctx: Context<Serum3CreateOpenOrders>) -> Result
let serum_market = ctx.accounts.serum_market.load()?;
let mut account = ctx.accounts.account.load_mut()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
let serum_account = account.serum3.create(serum_market.market_index)?;
serum_account.open_orders = ctx.accounts.open_orders.key();
serum_account.base_token_index = serum_market.base_token_index;

View File

@ -73,7 +73,7 @@ pub fn serum3_liq_force_cancel_orders(
//
{
let account = ctx.accounts.account.load()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
let serum_market = ctx.accounts.serum_market.load()?;
// Validate open_orders

View File

@ -168,7 +168,7 @@ pub fn serum3_place_order(
//
{
let account = ctx.accounts.account.load()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Validate open_orders
require!(

View File

@ -74,6 +74,7 @@ pub fn serum3_register_market(
base_token_index: base_bank.token_index,
quote_token_index: quote_bank.token_index,
bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?,
padding: Default::default(),
reserved: Default::default(),
};

View File

@ -77,7 +77,7 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
//
{
let account = ctx.accounts.account.load()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
// Validate open_orders
require!(

View File

@ -60,7 +60,7 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
let (position, raw_token_index, active_token_index) =
account.tokens.get_mut_or_create(token_index)?;

View File

@ -40,11 +40,7 @@ pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
) -> Result<()> {
let mint_info = ctx.accounts.mint_info.load()?;
{
let total_banks = mint_info
.banks
.iter()
.filter(|bank| *bank != &Pubkey::default())
.count();
let total_banks = mint_info.num_banks();
require_eq!(total_banks * 2, ctx.remaining_accounts.len());
}

View File

@ -106,6 +106,14 @@ pub fn token_register(
require_eq!(bank_num, 0);
// Require token 0 to be in the insurance token
if token_index == QUOTE_TOKEN_INDEX {
require_keys_eq!(
ctx.accounts.group.load()?.insurance_mint,
ctx.accounts.mint.key()
);
}
let mut bank = ctx.accounts.bank.load_init()?;
*bank = Bank {
name: fill16_from_str(name)?,
@ -162,6 +170,7 @@ pub fn token_register(
token_index,
address_lookup_table_bank_index: alt_previous_size as u8,
address_lookup_table_oracle_index: alt_previous_size as u8 + 1,
padding: Default::default(),
reserved: Default::default(),
};

View File

@ -64,7 +64,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
let (position, raw_token_index, active_token_index) =
account.tokens.get_mut_or_create(token_index)?;

View File

@ -302,6 +302,14 @@ pub mod mango_v4 {
)
}
pub fn liq_token_bankruptcy(
ctx: Context<LiqTokenBankruptcy>,
liab_token_index: TokenIndex,
max_liab_transfer: I80F48,
) -> Result<()> {
instructions::liq_token_bankruptcy(ctx, liab_token_index, max_liab_transfer)
}
///
/// Perps
///
@ -314,7 +322,6 @@ pub mod mango_v4 {
oracle_config: OracleConfig,
base_token_index_opt: Option<TokenIndex>,
base_token_decimals: u8,
quote_token_index: TokenIndex,
quote_lot_size: i64,
base_lot_size: i64,
maint_asset_weight: f32,
@ -335,7 +342,6 @@ pub mod mango_v4 {
oracle_config,
base_token_index_opt,
base_token_decimals,
quote_token_index,
quote_lot_size,
base_lot_size,
maint_asset_weight,

View File

@ -13,9 +13,11 @@ pub const YEAR: I80F48 = I80F48!(31536000);
#[account(zero_copy)]
pub struct Bank {
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
pub name: [u8; 16],
pub group: Pubkey,
pub mint: Pubkey,
pub vault: Pubkey,
pub oracle: Pubkey,
@ -34,6 +36,14 @@ pub struct Bank {
pub cached_indexed_total_borrows: I80F48,
/// deposits/borrows for this bank
///
/// Note that these may become negative. It's perfectly fine for users to borrow one one bank
/// (increasing indexed_borrows there) and paying back on another (possibly decreasing indexed_borrows
/// below zero).
///
/// The vault amount is not deducable from these values.
///
/// These become meaningful when summed over all banks (like in update_index).
pub indexed_deposits: I80F48,
pub indexed_borrows: I80F48,
@ -200,14 +210,28 @@ impl Bank {
require!(native_amount >= 0, MangoError::SomeError);
let native_position = position.native(self);
// Adding DELTA to amount/index helps because (amount/index)*index <= amount, but
// we want to ensure that users can withdraw the same amount they have deposited, so
// (amount/index + delta)*index >= amount is a better guarantee.
// Additionally, we require that we don't adjust values if
// (native / index) * index == native, because we sometimes call this function with
// values that are products of index.
let div_rounding_up = |native: I80F48, index: I80F48| {
let indexed = cm!(native / index);
if cm!(indexed * index) < native {
cm!(indexed + I80F48::DELTA)
} else {
indexed
}
};
if native_position.is_negative() {
let new_native_position = cm!(native_position + native_amount);
let indexed_change = cm!(native_amount / self.borrow_index + I80F48::DELTA);
let indexed_change = div_rounding_up(native_amount, self.borrow_index);
// this is only correct if it's not positive, because it scales the whole amount by borrow_index
let new_indexed_value = cm!(position.indexed_position + indexed_change);
if new_indexed_value.is_negative() {
// pay back borrows only, leaving a negative position
let indexed_change = cm!(native_amount / self.borrow_index + I80F48::DELTA);
self.indexed_borrows = cm!(self.indexed_borrows - indexed_change);
position.indexed_position = cm!(position.indexed_position + indexed_change);
return Ok(true);
@ -227,10 +251,7 @@ impl Bank {
}
// add to deposits
// Adding DELTA to amount/index helps because (amount/index)*index <= amount, but
// we want to ensure that users can withdraw the same amount they have deposited, so
// (amount/index + delta)*index >= amount is a better guarantee.
let indexed_change = cm!(native_amount / self.deposit_index + I80F48::DELTA);
let indexed_change = div_rounding_up(native_amount, self.deposit_index);
self.indexed_deposits = cm!(self.indexed_deposits + indexed_change);
position.indexed_position = cm!(position.indexed_position + indexed_change);

View File

@ -4,23 +4,30 @@ use std::mem::size_of;
// TODO: Assuming we allow up to 65536 different tokens
pub type TokenIndex = u16;
pub const QUOTE_TOKEN_INDEX: TokenIndex = 0;
#[account(zero_copy)]
#[derive(Debug)]
pub struct Group {
// Relying on Anchor's discriminator be sufficient for our versioning needs?
// pub meta_data: MetaData,
// ABI: Clients rely on this being at offset 8
pub admin: Pubkey,
// ABI: Clients rely on this being at offset 40
pub group_num: u32,
pub padding: [u8; 4],
pub insurance_vault: Pubkey,
pub insurance_mint: Pubkey,
pub bump: u8,
// Only support closing/deregistering groups, stub oracles, tokens, and markets
// if testing == 1
pub testing: u8,
pub padding: [u8; 2],
pub group_num: u32,
pub padding2: [u8; 6],
pub reserved: [u8; 8],
}
const_assert_eq!(size_of::<Group>(), 48);
const_assert_eq!(size_of::<Group>(), 32 * 3 + 4 + 4 + 1 * 2 + 6 + 8);
const_assert_eq!(size_of::<Group>() % 8, 0);
#[macro_export]

View File

@ -1,6 +1,7 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use serum_dex::state::OpenOrders;
use std::collections::HashMap;
@ -11,6 +12,8 @@ use crate::serum3_cpi;
use crate::state::{oracle_price, Bank, MangoAccount, PerpMarket, PerpMarketIndex, TokenIndex};
use crate::util::checked_math as cm;
const BANKRUPTCY_DUST_THRESHOLD: I80F48 = I80F48!(0.000001);
/// This trait abstracts how to find accounts needed for the health computation.
///
/// There are different ways they are retrieved from remainingAccounts, based
@ -216,7 +219,17 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
&mut self,
token_index1: TokenIndex,
token_index2: TokenIndex,
) -> Result<(&mut Bank, &mut Bank, I80F48, I80F48)> {
) -> Result<(&mut Bank, I80F48, Option<(&mut Bank, I80F48)>)> {
let n_banks = self.n_banks();
if token_index1 == token_index2 {
let index = self.bank_index(token_index1)?;
let (bank_part, oracle_part) = self.ais.split_at_mut(index + 1);
let bank = bank_part[index].load_mut_fully_unchecked::<Bank>()?;
let oracle = &oracle_part[n_banks - 1];
require!(&bank.oracle == oracle.key, MangoError::SomeError);
let price = oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?;
return Ok((bank, price, None));
}
let index1 = self.bank_index(token_index1)?;
let index2 = self.bank_index(token_index2)?;
let (first, second, swap) = if index1 < index2 {
@ -224,7 +237,6 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
} else {
(index2, index1, true)
};
let n_banks = self.n_banks();
// split_at_mut after the first bank and after the second bank
let (first_bank_part, second_part) = self.ais.split_at_mut(first + 1);
@ -241,9 +253,9 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let price1 = oracle_price(oracle1, bank1.oracle_config.conf_filter, mint_decimals1)?;
let price2 = oracle_price(oracle2, bank2.oracle_config.conf_filter, mint_decimals2)?;
if swap {
Ok((bank2, bank1, price2, price1))
Ok((bank2, price2, Some((bank1, price1))))
} else {
Ok((bank1, bank2, price1, price2))
Ok((bank1, price1, Some((bank2, price2))))
}
}
@ -493,6 +505,28 @@ impl HealthCache {
entry.balance = cm!(entry.balance + change * entry.oracle_price);
Ok(())
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self.token_infos.iter().any(|ti| {
ti.balance > BANKRUPTCY_DUST_THRESHOLD || ti.serum3_max_reserved.is_positive()
});
let perp_liquidatable = self
.perp_infos
.iter()
.any(|p| p.base != 0 || p.quote > BANKRUPTCY_DUST_THRESHOLD);
spot_liquidatable || perp_liquidatable
}
pub fn has_borrows(&self) -> bool {
// AUDIT: Can we really guarantee that liquidation/bankruptcy resolution always leaves
// non-negative balances?
let spot_borrows = self.token_infos.iter().any(|ti| ti.balance.is_negative());
let perp_borrows = self
.perp_infos
.iter()
.any(|p| p.quote.is_negative() || p.base != 0);
spot_borrows || perp_borrows
}
}
/// Compute health contribution for a given balance
@ -835,7 +869,6 @@ mod tests {
perp1.data().group = group;
perp1.data().perp_market_index = 9;
perp1.data().base_token_index = 4;
perp1.data().quote_token_index = 1;
perp1.data().init_asset_weight = I80F48::from_num(1.0 - 0.2f64);
perp1.data().init_liab_weight = I80F48::from_num(1.0 + 0.2f64);
perp1.data().maint_asset_weight = I80F48::from_num(1.0 - 0.1f64);
@ -914,7 +947,8 @@ mod tests {
assert_eq!(retriever.perp_index_map.len(), 1);
{
let (b1, b2, o1, o2) = retriever.banks_mut_and_oracles(1, 4).unwrap();
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(1, 4).unwrap();
let (b2, o2) = opt_b2o2.unwrap();
assert_eq!(b1.token_index, 1);
assert_eq!(o1, I80F48::ONE);
assert_eq!(b2.token_index, 4);
@ -922,13 +956,21 @@ mod tests {
}
{
let (b1, b2, o1, o2) = retriever.banks_mut_and_oracles(4, 1).unwrap();
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(4, 1).unwrap();
let (b2, o2) = opt_b2o2.unwrap();
assert_eq!(b1.token_index, 4);
assert_eq!(o1, 5 * I80F48::ONE);
assert_eq!(b2.token_index, 1);
assert_eq!(o2, I80F48::ONE);
}
{
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(4, 4).unwrap();
assert!(opt_b2o2.is_none());
assert_eq!(b1.token_index, 4);
assert_eq!(o1, 5 * I80F48::ONE);
}
retriever.banks_mut_and_oracles(4, 2).unwrap_err();
let oo = retriever.serum_oo(0, &oo1key).unwrap();
@ -1001,7 +1043,6 @@ mod tests {
perp1.data().group = group;
perp1.data().perp_market_index = 9;
perp1.data().base_token_index = 4;
perp1.data().quote_token_index = 1;
perp1.data().init_asset_weight = I80F48::from_num(1.0 - 0.2f64);
perp1.data().init_liab_weight = I80F48::from_num(1.0 + 0.2f64);
perp1.data().maint_asset_weight = I80F48::from_num(1.0 - 0.1f64);

View File

@ -704,11 +704,14 @@ impl Default for MangoAccountPerpPositions {
#[account(zero_copy)]
pub struct MangoAccount {
pub name: [u8; 32],
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
// ABI: Clients rely on this being at offset 40
pub owner: Pubkey,
pub name: [u8; 32],
// Alternative authority/signer of transactions for a mango account
pub delegate: Pubkey,
@ -723,10 +726,10 @@ pub struct MangoAccount {
pub perps: MangoAccountPerpPositions,
/// This account cannot open new positions or borrow until `init_health >= 0`
pub being_liquidated: u8,
being_liquidated: u8,
/// This account cannot do anything except go through `resolve_bankruptcy`
pub is_bankrupt: u8,
is_bankrupt: u8,
pub account_num: u8,
pub bump: u8,
@ -774,6 +777,22 @@ impl MangoAccount {
pub fn is_owner_or_delegate(&self, ix_signer: Pubkey) -> bool {
self.owner == ix_signer || self.delegate == ix_signer
}
pub fn is_bankrupt(&self) -> bool {
self.is_bankrupt != 0
}
pub fn set_bankrupt(&mut self, b: bool) {
self.is_bankrupt = if b { 1 } else { 0 };
}
pub fn being_liquidated(&self) -> bool {
self.being_liquidated != 0
}
pub fn set_being_liquidated(&mut self, b: bool) {
self.being_liquidated = if b { 1 } else { 0 };
}
}
impl Default for MangoAccount {

View File

@ -2,9 +2,9 @@ use anchor_lang::prelude::*;
use static_assertions::const_assert_eq;
use std::mem::size_of;
use crate::{accounts_zerocopy::LoadZeroCopyRef, error::MangoError};
use crate::error::MangoError;
use super::{Bank, TokenIndex};
use super::TokenIndex;
pub const MAX_BANKS: usize = 6;
@ -15,25 +15,28 @@ pub const MAX_BANKS: usize = 6;
#[account(zero_copy)]
#[derive(Debug)]
pub struct MintInfo {
// TODO: none of these pubkeys are needed, remove?
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
// ABI: Clients rely on this being at offset 40
pub token_index: TokenIndex,
pub padding: [u8; 6],
pub mint: Pubkey,
pub banks: [Pubkey; MAX_BANKS],
pub vaults: [Pubkey; MAX_BANKS],
pub oracle: Pubkey,
pub address_lookup_table: Pubkey,
pub token_index: TokenIndex,
// describe what address map relevant accounts are found on
pub address_lookup_table_bank_index: u8,
pub address_lookup_table_oracle_index: u8,
pub reserved: [u8; 4],
pub reserved: [u8; 6],
}
const_assert_eq!(
size_of::<MintInfo>(),
MAX_BANKS * 2 * 32 + 4 * 32 + 2 + 2 + 4
MAX_BANKS * 2 * 32 + 4 * 32 + 2 + 6 + 2 + 6
);
const_assert_eq!(size_of::<MintInfo>() % 8, 0);
@ -47,29 +50,22 @@ impl MintInfo {
self.vaults[0]
}
pub fn verify_banks_ais(&self, all_bank_ais: &[AccountInfo]) -> Result<()> {
let total_banks = self
.banks
pub fn num_banks(&self) -> usize {
self.banks
.iter()
.filter(|bank| *bank != &Pubkey::default())
.count();
require_eq!(total_banks, all_bank_ais.len());
.position(|&b| b == Pubkey::default())
.unwrap_or(MAX_BANKS)
}
for (idx, ai) in all_bank_ais.iter().enumerate() {
match ai.load::<Bank>() {
Ok(bank) => {
if self.token_index != bank.token_index
|| self.group != bank.group
// todo: just below check should be enough, above 2 checks are superfluous and defensive
|| self.banks[idx] != ai.key()
{
return Err(error!(MangoError::SomeError));
}
}
Err(error) => return Err(error),
}
}
pub fn banks(&self) -> &[Pubkey] {
&self.banks[..self.num_banks()]
}
pub fn verify_banks_ais(&self, all_bank_ais: &[AccountInfo]) -> Result<()> {
require!(
all_bank_ais.iter().map(|ai| ai.key).eq(self.banks().iter()),
MangoError::SomeError
);
Ok(())
}
}

View File

@ -71,7 +71,9 @@ pub enum OracleType {
#[account(zero_copy)]
pub struct StubOracle {
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
// ABI: Clients rely on this being at offset 40
pub mint: Pubkey,
pub price: I80F48,
pub last_updated: i64,

View File

@ -16,10 +16,20 @@ pub type PerpMarketIndex = u16;
#[account(zero_copy)]
#[derive(Debug)]
pub struct PerpMarket {
pub name: [u8; 16],
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
// TODO: Remove!
// ABI: Clients rely on this being at offset 40
pub base_token_index: TokenIndex,
/// Lookup indices
pub perp_market_index: PerpMarketIndex,
pub padding: [u8; 4],
pub name: [u8; 16],
pub oracle: Pubkey,
pub oracle_config: OracleConfig,
@ -76,18 +86,12 @@ pub struct PerpMarket {
pub base_token_decimals: u8,
/// Lookup indices
pub perp_market_index: PerpMarketIndex,
pub base_token_index: TokenIndex,
/// Cannot be chosen freely, must be the health-reference token, same for all PerpMarkets
pub quote_token_index: TokenIndex,
pub reserved: [u8; 6],
}
const_assert_eq!(
size_of::<PerpMarket>(),
16 + 32 * 2 + 16 + 32 * 3 + 8 * 2 + 16 * 11 + 8 * 2 + 8 * 2 + 16 + 8
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 11 + 8 * 2 + 8 * 2 + 16 + 2 + 6
);
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);

View File

@ -9,19 +9,26 @@ pub type Serum3MarketIndex = u16;
#[account(zero_copy)]
#[derive(Debug)]
pub struct Serum3Market {
pub name: [u8; 16],
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
// ABI: Clients rely on this being at offset 40
pub base_token_index: TokenIndex,
// ABI: Clients rely on this being at offset 42
pub quote_token_index: TokenIndex,
pub padding: [u8; 4],
pub name: [u8; 16],
pub serum_program: Pubkey,
pub serum_market_external: Pubkey,
pub market_index: Serum3MarketIndex,
pub base_token_index: TokenIndex,
pub quote_token_index: TokenIndex,
pub bump: u8,
pub reserved: [u8; 1],
pub reserved: [u8; 5],
}
const_assert_eq!(size_of::<Serum3Market>(), 16 + 32 * 3 + 3 * 2 + 1 + 1);
const_assert_eq!(
size_of::<Serum3Market>(),
32 + 2 + 2 + 4 + 16 + 2 * 32 + 2 + 1 + 5
);
const_assert_eq!(size_of::<Serum3Market>() % 8, 0);
impl Serum3Market {

View File

@ -231,7 +231,9 @@ async fn derive_liquidation_remaining_account_metas(
liqee: &MangoAccount,
liqor: &MangoAccount,
asset_token_index: TokenIndex,
asset_bank_index: usize,
liab_token_index: TokenIndex,
liab_bank_index: usize,
) -> Vec<AccountMeta> {
let mut banks = vec![];
let mut oracles = vec![];
@ -243,7 +245,13 @@ async fn derive_liquidation_remaining_account_metas(
.unique();
for token_index in token_indexes {
let mint_info = get_mint_info_by_token_index(account_loader, liqee, token_index).await;
let writable_bank = token_index == asset_token_index || token_index == liab_token_index;
let (bank_index, writable_bank) = if token_index == asset_token_index {
(asset_bank_index, true)
} else if token_index == liab_token_index {
(liab_bank_index, true)
} else {
(0, false)
};
// TODO: ALTs are unavailable
// let lookup_table = account_loader
// .load_bytes(&mint_info.address_lookup_table)
@ -255,7 +263,7 @@ async fn derive_liquidation_remaining_account_metas(
// writable_bank,
// ));
// oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
banks.push((mint_info.first_bank(), writable_bank));
banks.push((mint_info.banks[bank_index], writable_bank));
oracles.push(mint_info.oracle);
}
@ -306,6 +314,12 @@ pub async fn account_position(solana: &SolanaCookie, account: Pubkey, bank: Pubk
native.round().to_num::<i64>()
}
pub async fn account_position_closed(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> bool {
let account_data: MangoAccount = solana.get_account(account).await;
let bank_data: Bank = solana.get_account(bank).await;
account_data.tokens.find(bank_data.token_index).is_none()
}
pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> f64 {
let account_data: MangoAccount = solana.get_account(account).await;
let bank_data: Bank = solana.get_account(bank).await;
@ -627,6 +641,7 @@ pub struct TokenWithdrawInstruction<'keypair> {
pub account: Pubkey,
pub owner: &'keypair Keypair,
pub token_account: Pubkey,
pub bank_index: usize,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
@ -659,7 +674,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(mint_info.first_bank()),
Some(mint_info.banks[self.bank_index]),
false,
None,
)
@ -669,8 +684,8 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
group: account.group,
account: self.account,
owner: self.owner.pubkey(),
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
bank: mint_info.banks[self.bank_index],
vault: mint_info.vaults[self.bank_index],
token_account: self.token_account,
token_program: Token::id(),
};
@ -686,15 +701,16 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
}
}
pub struct TokenDepositInstruction<'keypair> {
pub struct TokenDepositInstruction {
pub amount: u64,
pub account: Pubkey,
pub token_account: Pubkey,
pub token_authority: &'keypair Keypair,
pub token_authority: Keypair,
pub bank_index: usize,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
impl ClientInstruction for TokenDepositInstruction {
type Accounts = mango_v4::accounts::TokenDeposit;
type Instruction = mango_v4::instruction::TokenDeposit;
async fn to_instruction(
@ -723,7 +739,7 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(mint_info.first_bank()),
Some(mint_info.banks[self.bank_index]),
false,
None,
)
@ -732,8 +748,8 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
let accounts = Self::Accounts {
group: account.group,
account: self.account,
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
bank: mint_info.banks[self.bank_index],
vault: mint_info.vaults[self.bank_index],
token_account: self.token_account,
token_authority: self.token_authority.pubkey(),
token_program: Token::id(),
@ -746,7 +762,7 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.token_authority]
vec![&self.token_authority]
}
}
@ -877,7 +893,6 @@ pub struct TokenAddBankInstruction<'keypair> {
pub group: Pubkey,
pub admin: &'keypair Keypair,
pub mint: Pubkey,
pub address_lookup_table: Pubkey,
pub payer: &'keypair Keypair,
}
@ -887,7 +902,7 @@ impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> {
type Instruction = mango_v4::instruction::TokenAddBank;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
@ -925,12 +940,12 @@ impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> {
&program_id,
)
.0;
let existing_bank_data: Bank = account_loader.load(&existing_bank).await.unwrap();
let mint = existing_bank_data.mint;
let mint_info = Pubkey::find_program_address(
&[
self.group.as_ref(),
b"MintInfo".as_ref(),
self.mint.as_ref(),
],
&[self.group.as_ref(), b"MintInfo".as_ref(), mint.as_ref()],
&program_id,
)
.0;
@ -938,7 +953,7 @@ impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> {
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
mint: self.mint,
mint: mint,
existing_bank,
bank,
vault,
@ -1172,6 +1187,7 @@ impl<'keypair> ClientInstruction for CloseStubOracleInstruction<'keypair> {
pub struct CreateGroupInstruction<'keypair> {
pub admin: &'keypair Keypair,
pub payer: &'keypair Keypair,
pub insurance_mint: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
@ -1197,11 +1213,21 @@ impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
)
.0;
let insurance_vault = Pubkey::find_program_address(
&[group.as_ref(), b"InsuranceVault".as_ref()],
&program_id,
)
.0;
let accounts = Self::Accounts {
group,
admin: self.admin.pubkey(),
insurance_mint: self.insurance_mint,
insurance_vault,
payer: self.payer.pubkey(),
token_program: Token::id(),
system_program: System::id(),
rent: sysvar::rent::Rent::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
@ -1984,7 +2010,9 @@ pub struct LiqTokenWithTokenInstruction<'keypair> {
pub liqor_owner: &'keypair Keypair,
pub asset_token_index: TokenIndex,
pub asset_bank_index: usize,
pub liab_token_index: TokenIndex,
pub liab_bank_index: usize,
pub max_liab_transfer: I80F48,
}
#[async_trait::async_trait(?Send)]
@ -2009,7 +2037,9 @@ impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> {
&liqee,
&liqor,
self.asset_token_index,
self.asset_bank_index,
self.liab_token_index,
self.liab_bank_index,
)
.await;
@ -2031,6 +2061,94 @@ impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> {
}
}
pub struct LiqTokenBankruptcyInstruction<'keypair> {
pub liqee: Pubkey,
pub liqor: Pubkey,
pub liqor_owner: &'keypair Keypair,
pub liab_token_index: TokenIndex,
pub max_liab_transfer: I80F48,
pub liab_mint_info: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for LiqTokenBankruptcyInstruction<'keypair> {
type Accounts = mango_v4::accounts::LiqTokenBankruptcy;
type Instruction = mango_v4::instruction::LiqTokenBankruptcy;
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 {
liab_token_index: self.liab_token_index,
max_liab_transfer: self.max_liab_transfer,
};
let liab_mint_info: MintInfo = account_loader.load(&self.liab_mint_info).await.unwrap();
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,
QUOTE_TOKEN_INDEX,
0,
self.liab_token_index,
0,
)
.await;
let group: Group = account_loader.load(&liqee.group).await.unwrap();
let quote_mint_info = Pubkey::find_program_address(
&[
liqee.group.as_ref(),
b"MintInfo".as_ref(),
group.insurance_mint.as_ref(),
],
&program_id,
)
.0;
let quote_mint_info: MintInfo = account_loader.load(&quote_mint_info).await.unwrap();
let insurance_vault = Pubkey::find_program_address(
&[liqee.group.as_ref(), b"InsuranceVault".as_ref()],
&program_id,
)
.0;
let accounts = Self::Accounts {
group: liqee.group,
liqee: self.liqee,
liqor: self.liqor,
liqor_owner: self.liqor_owner.pubkey(),
liab_mint_info: self.liab_mint_info,
quote_vault: quote_mint_info.first_vault(),
insurance_vault,
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
let mut bank_ams = liab_mint_info
.banks()
.iter()
.map(|bank| AccountMeta {
pubkey: *bank,
is_signer: false,
is_writable: true,
})
.collect::<Vec<_>>();
instruction.accounts.append(&mut bank_ams);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.liqor_owner]
}
}
pub struct PerpCreateMarketInstruction<'keypair> {
pub group: Pubkey,
pub admin: &'keypair Keypair,
@ -2042,7 +2160,6 @@ pub struct PerpCreateMarketInstruction<'keypair> {
pub perp_market_index: PerpMarketIndex,
pub base_token_index: TokenIndex,
pub base_token_decimals: u8,
pub quote_token_index: TokenIndex,
pub quote_lot_size: i64,
pub base_lot_size: i64,
pub maint_asset_weight: f32,
@ -2069,7 +2186,6 @@ impl<'keypair> ClientInstruction for PerpCreateMarketInstruction<'keypair> {
},
perp_market_index: self.perp_market_index,
base_token_index_opt: Option::from(self.base_token_index),
quote_token_index: self.quote_token_index,
quote_lot_size: self.quote_lot_size,
base_lot_size: self.base_lot_size,
maint_asset_weight: self.maint_asset_weight,
@ -2431,8 +2547,6 @@ impl ClientInstruction for BenchmarkInstruction {
}
pub struct UpdateIndexInstruction {
pub mint_info: Pubkey,
pub oracle: Pubkey,
pub banks: Vec<Pubkey>,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for UpdateIndexInstruction {
@ -2440,20 +2554,22 @@ impl ClientInstruction for UpdateIndexInstruction {
type Instruction = mango_v4::instruction::UpdateIndex;
async fn to_instruction(
&self,
_loader: impl ClientAccountLoader + 'async_trait,
loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let mint_info: MintInfo = loader.load(&self.mint_info).await.unwrap();
let accounts = Self::Accounts {
mint_info: self.mint_info,
oracle: self.oracle,
oracle: mint_info.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
let mut bank_ams = self
.banks
let mut bank_ams = mint_info
.banks()
.iter()
.filter(|bank| **bank != Pubkey::default())
.map(|bank| AccountMeta {
pubkey: *bank,
is_signer: false,

View File

@ -18,12 +18,14 @@ pub struct Token {
pub mint: MintCookie,
pub oracle: Pubkey,
pub bank: Pubkey,
pub bank1: Pubkey,
pub vault: Pubkey,
pub mint_info: Pubkey,
}
pub struct GroupWithTokens {
pub group: Pubkey,
pub insurance_vault: Pubkey,
pub tokens: Vec<Token>,
}
@ -34,10 +36,18 @@ impl<'a> GroupWithTokensConfig<'a> {
payer,
mints,
} = self;
let group = send_tx(solana, CreateGroupInstruction { admin, payer })
.await
.unwrap()
.group;
let create_group_accounts = send_tx(
solana,
CreateGroupInstruction {
admin,
payer,
insurance_mint: mints[0].pubkey,
},
)
.await
.unwrap();
let group = create_group_accounts.group;
let insurance_vault = create_group_accounts.insurance_vault;
let address_lookup_table = solana.create_address_lookup_table(admin, payer).await;
@ -94,14 +104,13 @@ impl<'a> GroupWithTokensConfig<'a> {
)
.await
.unwrap();
let _ = send_tx(
let add_bank_accounts = send_tx(
solana,
TokenAddBankInstruction {
token_index,
bank_num: 1,
group,
admin,
mint: mint.pubkey,
address_lookup_table,
payer,
},
@ -117,11 +126,16 @@ impl<'a> GroupWithTokensConfig<'a> {
mint: mint.clone(),
oracle,
bank,
bank1: add_bank_accounts.bank,
vault,
mint_info,
});
}
GroupWithTokens { group, tokens }
GroupWithTokens {
group,
insurance_vault,
tokens,
}
}
}

View File

@ -2,6 +2,7 @@ use bytemuck::{bytes_of, Contiguous};
use solana_program::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use std::ops::Deref;
#[allow(dead_code)]
pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] {
@ -32,3 +33,47 @@ pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pub
pub fn clone_keypair(keypair: &Keypair) -> Keypair {
Keypair::from_base58_string(&keypair.to_base58_string())
}
// Add clone() to Keypair, totally safe in tests
pub trait ClonableKeypair {
fn clone(&self) -> Self;
}
impl ClonableKeypair for Keypair {
fn clone(&self) -> Self {
clone_keypair(self)
}
}
// Make a clonable and defaultable Keypair newtype
pub struct TestKeypair(pub Keypair);
impl Clone for TestKeypair {
fn clone(&self) -> Self {
TestKeypair(self.0.clone())
}
}
impl Default for TestKeypair {
fn default() -> Self {
TestKeypair(Keypair::from_bytes(&[0u8; 64]).unwrap())
}
}
impl AsRef<Keypair> for TestKeypair {
fn as_ref(&self) -> &Keypair {
&self.0
}
}
impl Deref for TestKeypair {
type Target = Keypair;
fn deref(&self) -> &Keypair {
&self.0
}
}
impl From<&Keypair> for TestKeypair {
fn from(k: &Keypair) -> Self {
Self(k.clone())
}
}
impl From<Keypair> for TestKeypair {
fn from(k: Keypair) -> Self {
Self(k)
}
}

View File

@ -0,0 +1,654 @@
#![cfg(feature = "test-bpf")]
use fixed::types::I80F48;
use solana_program_test::*;
use solana_sdk::{
signature::{Keypair, Signer},
transport::TransportError,
};
use mango_v4::state::*;
use program_test::*;
mod program_test;
#[tokio::test]
async fn test_bankrupt_tokens_socialize_loss() -> 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..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,
}
.create(solana)
.await;
let borrow_token1 = &tokens[0];
let borrow_token2 = &tokens[1];
let collateral_token1 = &tokens[2];
let collateral_token2 = &tokens[3];
// 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;
let vault_amount = 100000;
for &token_account in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: vault_amount,
account: vault_account,
token_account,
token_authority: payer.clone(),
bank_index: 1,
},
)
.await
.unwrap();
}
// also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss
send_tx(
solana,
TokenDepositInstruction {
amount: 10,
account: vault_account,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: Make an account with some collateral and some borrows
//
let account = send_tx(
solana,
CreateAccountInstruction {
account_num: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let deposit1_amount = 1000;
let deposit2_amount = 20;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit1_amount,
account,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit2_amount,
account,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
let borrow1_amount = 350;
let borrow1_amount_bank0 = 10;
let borrow1_amount_bank1 = borrow1_amount - borrow1_amount_bank0;
let borrow2_amount = 50;
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow1_amount_bank1,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 1,
},
)
.await
.unwrap();
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow1_amount_bank0,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow2_amount,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 1,
},
)
.await
.unwrap();
//
// SETUP: Change the oracle to make health go very negative
//
send_tx(
solana,
SetStubOracleInstruction {
group,
admin,
mint: borrow_token1.mint.pubkey,
payer,
price: "20.0",
},
)
.await
.unwrap();
//
// SETUP: liquidate all the collateral against borrow1
//
// eat collateral1
send_tx(
solana,
LiqTokenWithTokenInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
asset_token_index: collateral_token1.index,
asset_bank_index: 1,
liab_token_index: borrow_token1.index,
liab_bank_index: 1,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
0
);
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
(-350.0f64 + (1000.0 / 20.0 / 1.04)).round() as i64
);
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
// eat collateral2, leaving the account bankrupt
send_tx(
solana,
LiqTokenWithTokenInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
asset_token_index: collateral_token2.index,
asset_bank_index: 1,
liab_token_index: borrow_token1.index,
liab_bank_index: 1,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, account, collateral_token2.bank).await,
0
);
let borrow1_after_liq = -350.0f64 + (1000.0 / 20.0 / 1.04) + (20.0 / 20.0 / 1.04);
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
borrow1_after_liq.round() as i64
);
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(liqee.is_bankrupt());
//
// TEST: socialize loss on borrow1 and 2
//
let vault_before = account_position(solana, vault_account, borrow_token1.bank).await;
send_tx(
solana,
LiqTokenBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_token_index: borrow_token1.index,
liab_mint_info: borrow_token1.mint_info,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
vault_before + (borrow1_after_liq.round() as i64)
);
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(liqee.is_bankrupt());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
// both bank's borrows were completely wiped: no one else borrowed
let borrow1_bank0: Bank = solana.get_account(borrow_token1.bank).await;
let borrow1_bank1: Bank = solana.get_account(borrow_token1.bank).await;
assert_eq!(borrow1_bank0.native_borrows(), 0);
assert_eq!(borrow1_bank1.native_borrows(), 0);
send_tx(
solana,
LiqTokenBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_token_index: borrow_token2.index,
liab_mint_info: borrow_token2.mint_info,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, vault_account, borrow_token2.bank).await,
(vault_amount - borrow2_amount) as i64
);
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated()); // TODO: no longer being liquidated?
assert!(!liqee.is_bankrupt());
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
Ok(())
}
#[tokio::test]
async fn test_bankrupt_tokens_insurance_fund() -> 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..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,
insurance_vault,
} = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let borrow_token1 = &tokens[0]; // USDC
let borrow_token2 = &tokens[1];
let collateral_token1 = &tokens[2];
let collateral_token2 = &tokens[3];
// fund the insurance vault
{
let mut tx = ClientTransaction::new(solana);
tx.add_instruction_direct(
spl_token::instruction::transfer(
&spl_token::ID,
&payer_mint_accounts[0],
&insurance_vault,
&payer.pubkey(),
&[&payer.pubkey()],
1051,
)
.unwrap(),
);
tx.add_signer(payer);
tx.send().await.unwrap();
}
// 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;
let vault_amount = 100000;
for &token_account in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: vault_amount,
account: vault_account,
token_account,
token_authority: payer.clone(),
bank_index: 1,
},
)
.await
.unwrap();
}
// also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss
send_tx(
solana,
TokenDepositInstruction {
amount: 10,
account: vault_account,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: Make an account with some collateral and some borrows
//
let account = send_tx(
solana,
CreateAccountInstruction {
account_num: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let deposit1_amount = 20;
let deposit2_amount = 1000;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit1_amount,
account,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit2_amount,
account,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
let borrow1_amount = 50;
let borrow1_amount_bank0 = 10;
let borrow1_amount_bank1 = borrow1_amount - borrow1_amount_bank0;
let borrow2_amount = 350;
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow1_amount_bank1,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 1,
},
)
.await
.unwrap();
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow1_amount_bank0,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow2_amount,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 1,
},
)
.await
.unwrap();
//
// SETUP: Change the oracle to make health go very negative
//
send_tx(
solana,
SetStubOracleInstruction {
group,
admin,
mint: borrow_token2.mint.pubkey,
payer,
price: "20.0",
},
)
.await
.unwrap();
//
// SETUP: liquidate all the collateral against borrow2
//
// eat collateral1
send_tx(
solana,
LiqTokenWithTokenInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
asset_token_index: collateral_token1.index,
asset_bank_index: 1,
liab_token_index: borrow_token2.index,
liab_bank_index: 1,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
0
);
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
// eat collateral2, leaving the account bankrupt
send_tx(
solana,
LiqTokenWithTokenInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
asset_token_index: collateral_token2.index,
asset_bank_index: 1,
liab_token_index: borrow_token2.index,
liab_bank_index: 1,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, account, collateral_token2.bank).await,
0
);
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(liqee.is_bankrupt());
//
// TEST: use the insurance fund to liquidate borrow1 and borrow2
//
// bankruptcy of an USDC liability: just transfers funds from insurance vault to liqee,
// the liqor is uninvolved
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
send_tx(
solana,
LiqTokenBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_token_index: borrow_token1.index,
liab_mint_info: borrow_token1.mint_info,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(liqee.is_bankrupt());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
assert_eq!(
solana.token_account_balance(insurance_vault).await,
// the loan origination fees push the borrow above 50.0 and cause this rounding
insurance_vault_before - borrow1_amount - 1
);
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
liqor_before
);
// bankruptcy of a non-USDC liability: USDC to liqor, liability to liqee
// liquidating only a partial amount
let liab_before = account_position_f64(solana, account, borrow_token2.bank).await;
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
let liab_transfer: f64 = 500.0 / 20.0;
send_tx(
solana,
LiqTokenBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_token_index: borrow_token2.index,
liab_mint_info: borrow_token2.mint_info,
max_liab_transfer: I80F48::from_num(liab_transfer),
},
)
.await
.unwrap();
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(liqee.is_bankrupt());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
assert_eq!(
account_position(solana, account, borrow_token2.bank).await,
(liab_before + liab_transfer) as i64
);
let usdc_amount = (liab_transfer * 20.0 * 1.02).ceil() as u64;
assert_eq!(
solana.token_account_balance(insurance_vault).await,
insurance_vault_before - usdc_amount
);
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
liqor_before + usdc_amount as i64
);
// bankruptcy of a non-USDC liability: USDC to liqor, liability to liqee
// liquidating fully and then doing socialized loss because the insurance fund is exhausted
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
send_tx(
solana,
LiqTokenBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_token_index: borrow_token2.index,
liab_mint_info: borrow_token2.mint_info,
max_liab_transfer: I80F48::from_num(1000000.0),
},
)
.await
.unwrap();
let liqee: MangoAccount = solana.get_account(account).await;
assert!(liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
liqor_before + insurance_vault_before as i64
);
Ok(())
}

View File

@ -27,7 +27,7 @@ async fn test_basic() -> Result<(), TransportError> {
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -63,7 +63,8 @@ async fn test_basic() -> Result<(), TransportError> {
amount: deposit_amount,
account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -111,6 +112,7 @@ async fn test_basic() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await
@ -136,15 +138,11 @@ async fn test_basic() -> Result<(), TransportError> {
// TEST: Close account and de register bank
//
let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await;
// withdraw whatever is remaining, can't close bank vault without this
send_tx(
solana,
UpdateIndexInstruction {
mint_info: tokens[0].mint_info,
banks: mint_info.banks.to_vec(),
oracle: mint_info.oracle,
},
)
.await
@ -158,6 +156,7 @@ async fn test_basic() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await

View File

@ -24,7 +24,7 @@ async fn test_delegate() -> Result<(), TransportError> {
// SETUP: Create a group, register a token (mint0), create an account
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -53,7 +53,8 @@ async fn test_delegate() -> Result<(), TransportError> {
amount: 100,
account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -108,6 +109,7 @@ async fn test_delegate() -> Result<(), TransportError> {
account,
owner: delegate,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await;
@ -127,6 +129,7 @@ async fn test_delegate() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await

View File

@ -59,7 +59,8 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
amount: deposit_amount,
account,
token_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -88,7 +89,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -169,7 +170,8 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
amount: 10,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -198,7 +200,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -226,7 +228,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
amount: 1000,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -268,7 +271,6 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
perp_market_index: perp_market_index as PerpMarketIndex,
base_token_index: quote_token.index,
base_token_decimals: quote_token.mint.decimals,
quote_token_index: token.index,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,
@ -323,7 +325,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
amount: 10,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await

View File

@ -27,7 +27,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
// SETUP: Create a group and an account to fill the vaults
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -57,7 +57,8 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
amount: 10000,
account: vault_account,
token_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -112,7 +113,8 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[1],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -179,6 +181,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
}
)
.await
@ -207,6 +210,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
@ -230,7 +234,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
// SETUP: Create a group and an account to fill the vaults
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -262,7 +266,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
amount: 100000,
account: vault_account,
token_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -293,7 +298,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
amount: deposit1_amount,
account,
token_account: payer_mint_accounts[2],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -304,7 +310,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
amount: deposit2_amount,
account,
token_account: payer_mint_accounts[3],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -320,6 +327,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
@ -332,6 +340,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
@ -365,6 +374,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
liqor_owner: owner,
asset_token_index: collateral_token2.index,
liab_token_index: borrow_token2.index,
asset_bank_index: 0,
liab_bank_index: 0,
max_liab_transfer: I80F48::from_num(10000.0),
},
)
@ -381,7 +392,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
0
);
let liqee: MangoAccount = solana.get_account(account).await;
assert_eq!(liqee.being_liquidated, 1);
assert!(liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
//
// TEST: liquidate the remaining borrow2 against collateral1,
@ -396,6 +408,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
asset_token_index: collateral_token1.index,
liab_token_index: borrow_token2.index,
max_liab_transfer: I80F48::from_num(10000.0),
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
@ -411,7 +425,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
1000 - 32
);
let liqee: MangoAccount = solana.get_account(account).await;
assert_eq!(liqee.being_liquidated, 1);
assert!(liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
//
// TEST: liquidate borrow1 with collateral1, but place a limit
@ -425,6 +440,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
asset_token_index: collateral_token1.index,
liab_token_index: borrow_token1.index,
max_liab_transfer: I80F48::from_num(10.0),
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
@ -440,7 +457,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
1000 - 32 - 21
);
let liqee: MangoAccount = solana.get_account(account).await;
assert_eq!(liqee.being_liquidated, 1);
assert!(liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
//
// TEST: liquidate borrow1 with collateral1, making the account healthy again
@ -454,6 +472,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
asset_token_index: collateral_token1.index,
liab_token_index: borrow_token1.index,
max_liab_transfer: I80F48::from_num(10000.0),
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
@ -472,7 +492,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
1000 - 32 - 535 - 1
);
let liqee: MangoAccount = solana.get_account(account).await;
assert_eq!(liqee.being_liquidated, 0);
assert!(!liqee.being_liquidated());
assert!(!liqee.is_bankrupt());
Ok(())
}

View File

@ -34,7 +34,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -68,7 +68,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
amount: provided_amount,
account: provider_account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -79,7 +80,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
amount: provided_amount,
account: provider_account,
token_account: payer_mint1_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -115,7 +117,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
amount: deposit_amount_initial,
account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -347,7 +350,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -381,7 +384,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
amount: provided_amount,
account: provider_account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -392,7 +396,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
amount: provided_amount,
account: provider_account,
token_account: payer_mint1_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -428,7 +433,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
amount: deposit_amount_initial,
account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -605,7 +611,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -639,7 +645,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
amount: provided_amount,
account: provider_account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -650,7 +657,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
amount: provided_amount,
account: provider_account,
token_account: payer_mint1_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -686,7 +694,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
amount: deposit_amount_initial,
account,
token_account: payer_mint0_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await

View File

@ -24,7 +24,7 @@ async fn test_perp() -> Result<(), TransportError> {
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -70,7 +70,8 @@ async fn test_perp() -> Result<(), TransportError> {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -82,7 +83,8 @@ async fn test_perp() -> Result<(), TransportError> {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[1],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -98,7 +100,8 @@ async fn test_perp() -> Result<(), TransportError> {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -110,7 +113,8 @@ async fn test_perp() -> Result<(), TransportError> {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[1],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -150,7 +154,6 @@ async fn test_perp() -> Result<(), TransportError> {
perp_market_index: 0,
base_token_index: tokens[0].index,
base_token_decimals: tokens[0].mint.decimals,
quote_token_index: tokens[1].index,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 0.975,

View File

@ -26,7 +26,7 @@ async fn test_position_lifetime() -> Result<()> {
// SETUP: Create a group and accounts
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -72,7 +72,8 @@ async fn test_position_lifetime() -> Result<()> {
amount: funding_amount,
account: funding_account,
token_account: payer_token,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -95,7 +96,8 @@ async fn test_position_lifetime() -> Result<()> {
amount: deposit_amount,
account,
token_account: payer_token,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -112,6 +114,7 @@ async fn test_position_lifetime() -> Result<()> {
account,
owner,
token_account: payer_token,
bank_index: 0,
},
)
.await
@ -145,7 +148,8 @@ async fn test_position_lifetime() -> Result<()> {
amount: collateral_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -161,6 +165,7 @@ async fn test_position_lifetime() -> Result<()> {
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
@ -179,7 +184,8 @@ async fn test_position_lifetime() -> Result<()> {
amount: borrow_amount + 2,
account,
token_account: payer_mint_accounts[1],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -193,6 +199,7 @@ async fn test_position_lifetime() -> Result<()> {
account,
owner,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
@ -208,6 +215,7 @@ async fn test_position_lifetime() -> Result<()> {
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await

View File

@ -26,7 +26,7 @@ async fn test_serum() -> Result<(), TransportError> {
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -69,7 +69,8 @@ async fn test_serum() -> Result<(), TransportError> {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -81,7 +82,8 @@ async fn test_serum() -> Result<(), TransportError> {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[1],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await

View File

@ -1,6 +1,6 @@
#![cfg(feature = "test-bpf")]
use mango_v4::state::{Bank, MintInfo};
use mango_v4::state::*;
use solana_program_test::*;
use solana_sdk::{signature::Keypair, transport::TransportError};
@ -23,7 +23,7 @@ async fn test_update_index() -> Result<(), TransportError> {
// SETUP: Create a group and an account to fill the vaults
//
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
@ -51,7 +51,8 @@ async fn test_update_index() -> Result<(), TransportError> {
amount: 10000,
account: deposit_account,
token_account,
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -77,7 +78,8 @@ async fn test_update_index() -> Result<(), TransportError> {
amount: 100000,
account: withdraw_account,
token_account: payer_mint_accounts[1],
token_authority: payer,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
@ -91,6 +93,7 @@ async fn test_update_index() -> Result<(), TransportError> {
account: withdraw_account,
owner,
token_account: context.users[0].token_accounts[0],
bank_index: 0,
},
)
.await
@ -100,14 +103,10 @@ async fn test_update_index() -> Result<(), TransportError> {
solana.advance_clock().await;
let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await;
send_tx(
solana,
UpdateIndexInstruction {
mint_info: tokens[0].mint_info,
banks: mint_info.banks.to_vec(),
oracle: mint_info.oracle,
},
)
.await

View File

@ -15,7 +15,7 @@ anchor build --skip-lint
# update types in ts client package
cp -v ./target/types/mango_v4.ts ./ts/client/src/mango_v4.ts
(cd ./ts/client && tsc)
(cd ./ts/client && yarn tsc)
if [[ -z "${NO_DEPLOY}" ]]; then
# publish program

View File

@ -15,7 +15,7 @@ anchor build --skip-lint
# update types in ts client package
cp -v ./target/types/mango_v4.ts ./ts/client/src/mango_v4.ts
(cd ./ts/client && tsc)
(cd ./ts/client && yarn tsc)
if [[ -z "${NO_DEPLOY}" ]]; then
# publish program
@ -31,4 +31,4 @@ fi
# build npm package
(cd ./ts/client && tsc)
(cd ./ts/client && yarn tsc)

View File

@ -67,6 +67,7 @@ export class MangoClient {
public async createGroup(
groupNum: number,
testing: boolean,
insuranceMintPk: PublicKey,
): Promise<TransactionSignature> {
const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey;
return await this.program.methods
@ -74,6 +75,7 @@ export class MangoClient {
.accounts({
admin: adminPk,
payer: adminPk,
insuranceMint: insuranceMintPk,
})
.rpc();
}
@ -117,7 +119,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 44,
offset: 40,
},
});
}
@ -286,7 +288,7 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
offset: 8,
},
},
])
@ -325,7 +327,7 @@ export class MangoClient {
{
memcmp: {
bytes: bs58.encode(tokenIndexBuf),
offset: 200,
offset: 40,
},
},
])
@ -443,6 +445,7 @@ export class MangoClient {
public async editMangoAccount(
group: Group,
mangoAccount: MangoAccount,
name?: string,
delegate?: PublicKey,
): Promise<TransactionSignature> {
@ -450,6 +453,7 @@ export class MangoClient {
.editAccount(name, delegate)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.rpc();
@ -471,13 +475,13 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 40,
offset: 8,
},
},
{
memcmp: {
bytes: ownerPk.toBase58(),
offset: 72,
offset: 40,
},
},
])
@ -724,7 +728,7 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
offset: 8,
},
},
];
@ -735,7 +739,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 122,
offset: 40,
},
});
}
@ -746,7 +750,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(qbuf),
offset: 124,
offset: 42,
},
});
}
@ -1052,7 +1056,6 @@ export class MangoClient {
} as any, // future: nested custom types dont typecheck, fix if possible?
baseTokenIndex,
baseTokenDecimals,
quoteTokenIndex,
new BN(quoteLotSize),
new BN(baseLotSize),
maintAssetWeight,
@ -1188,7 +1191,6 @@ export class MangoClient {
public async perpGetMarkets(
group: Group,
baseTokenIndex?: number,
quoteTokenIndex?: number,
): Promise<PerpMarket[]> {
const bumpfbuf = Buffer.alloc(1);
bumpfbuf.writeUInt8(255);
@ -1197,7 +1199,7 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
offset: 8,
},
},
];
@ -1208,18 +1210,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 444,
},
});
}
if (quoteTokenIndex) {
const qbuf = Buffer.alloc(2);
qbuf.writeUInt16LE(quoteTokenIndex);
filters.push({
memcmp: {
bytes: bs58.encode(qbuf),
offset: 446,
offset: 40,
},
});
}

View File

@ -34,15 +34,49 @@ export type MangoV4 = {
"isMut": false,
"isSigner": true
},
{
"name": "insuranceMint",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false,
"pda": {
"seeds": [
{
"kind": "account",
"type": "publicKey",
"path": "group"
},
{
"kind": "const",
"type": "string",
"value": "InsuranceVault"
}
]
}
},
{
"name": "payer",
"isMut": true,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
},
{
"name": "rent",
"isMut": false,
"isSigner": false
}
],
"args": [
@ -1769,6 +1803,63 @@ export type MangoV4 = {
}
]
},
{
"name": "liqTokenBankruptcy",
"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
},
{
"name": "liabMintInfo",
"isMut": false,
"isSigner": false
},
{
"name": "quoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "liabTokenIndex",
"type": "u16"
},
{
"name": "maxLiabTransfer",
"type": {
"defined": "I80F48"
}
}
]
},
{
"name": "perpCreateMarket",
"accounts": [
@ -1862,10 +1953,6 @@ export type MangoV4 = {
"name": "baseTokenDecimals",
"type": "u8"
},
{
"name": "quoteTokenIndex",
"type": "u16"
},
{
"name": "quoteLotSize",
"type": "i64"
@ -2405,6 +2492,10 @@ export type MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "name",
"type": {
@ -2414,10 +2505,6 @@ export type MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "mint",
"type": "publicKey"
@ -2605,6 +2692,27 @@ export type MangoV4 = {
"name": "admin",
"type": "publicKey"
},
{
"name": "groupNum",
"type": "u32"
},
{
"name": "padding",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "insuranceVault",
"type": "publicKey"
},
{
"name": "insuranceMint",
"type": "publicKey"
},
{
"name": "bump",
"type": "u8"
@ -2614,18 +2722,14 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "padding",
"name": "padding2",
"type": {
"array": [
"u8",
2
6
]
}
},
{
"name": "groupNum",
"type": "u32"
},
{
"name": "reserved",
"type": {
@ -2643,6 +2747,14 @@ export type MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "name",
"type": {
@ -2652,14 +2764,6 @@ export type MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "delegate",
"type": "publicKey"
@ -2719,6 +2823,19 @@ export type MangoV4 = {
"name": "group",
"type": "publicKey"
},
{
"name": "tokenIndex",
"type": "u16"
},
{
"name": "padding",
"type": {
"array": [
"u8",
6
]
}
},
{
"name": "mint",
"type": "publicKey"
@ -2749,10 +2866,6 @@ export type MangoV4 = {
"name": "addressLookupTable",
"type": "publicKey"
},
{
"name": "tokenIndex",
"type": "u16"
},
{
"name": "addressLookupTableBankIndex",
"type": "u8"
@ -2766,7 +2879,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
4
6
]
}
}
@ -2892,6 +3005,27 @@ export type MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "padding",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "name",
"type": {
@ -2901,10 +3035,6 @@ export type MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "oracle",
"type": "publicKey"
@ -3032,16 +3162,13 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "quoteTokenIndex",
"type": "u16"
"name": "reserved",
"type": {
"array": [
"u8",
6
]
}
}
]
}
@ -3051,6 +3178,27 @@ export type MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "quoteTokenIndex",
"type": "u16"
},
{
"name": "padding",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "name",
"type": {
@ -3060,10 +3208,6 @@ export type MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "serumProgram",
"type": "publicKey"
@ -3076,14 +3220,6 @@ export type MangoV4 = {
"name": "marketIndex",
"type": "u16"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "quoteTokenIndex",
"type": "u16"
},
{
"name": "bump",
"type": "u8"
@ -3093,7 +3229,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
1
5
]
}
}
@ -4426,15 +4562,49 @@ export const IDL: MangoV4 = {
"isMut": false,
"isSigner": true
},
{
"name": "insuranceMint",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false,
"pda": {
"seeds": [
{
"kind": "account",
"type": "publicKey",
"path": "group"
},
{
"kind": "const",
"type": "string",
"value": "InsuranceVault"
}
]
}
},
{
"name": "payer",
"isMut": true,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
},
{
"name": "rent",
"isMut": false,
"isSigner": false
}
],
"args": [
@ -6161,6 +6331,63 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "liqTokenBankruptcy",
"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
},
{
"name": "liabMintInfo",
"isMut": false,
"isSigner": false
},
{
"name": "quoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "liabTokenIndex",
"type": "u16"
},
{
"name": "maxLiabTransfer",
"type": {
"defined": "I80F48"
}
}
]
},
{
"name": "perpCreateMarket",
"accounts": [
@ -6254,10 +6481,6 @@ export const IDL: MangoV4 = {
"name": "baseTokenDecimals",
"type": "u8"
},
{
"name": "quoteTokenIndex",
"type": "u16"
},
{
"name": "quoteLotSize",
"type": "i64"
@ -6797,6 +7020,10 @@ export const IDL: MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "name",
"type": {
@ -6806,10 +7033,6 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "mint",
"type": "publicKey"
@ -6997,6 +7220,27 @@ export const IDL: MangoV4 = {
"name": "admin",
"type": "publicKey"
},
{
"name": "groupNum",
"type": "u32"
},
{
"name": "padding",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "insuranceVault",
"type": "publicKey"
},
{
"name": "insuranceMint",
"type": "publicKey"
},
{
"name": "bump",
"type": "u8"
@ -7006,18 +7250,14 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "padding",
"name": "padding2",
"type": {
"array": [
"u8",
2
6
]
}
},
{
"name": "groupNum",
"type": "u32"
},
{
"name": "reserved",
"type": {
@ -7035,6 +7275,14 @@ export const IDL: MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "name",
"type": {
@ -7044,14 +7292,6 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "owner",
"type": "publicKey"
},
{
"name": "delegate",
"type": "publicKey"
@ -7111,6 +7351,19 @@ export const IDL: MangoV4 = {
"name": "group",
"type": "publicKey"
},
{
"name": "tokenIndex",
"type": "u16"
},
{
"name": "padding",
"type": {
"array": [
"u8",
6
]
}
},
{
"name": "mint",
"type": "publicKey"
@ -7141,10 +7394,6 @@ export const IDL: MangoV4 = {
"name": "addressLookupTable",
"type": "publicKey"
},
{
"name": "tokenIndex",
"type": "u16"
},
{
"name": "addressLookupTableBankIndex",
"type": "u8"
@ -7158,7 +7407,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
4
6
]
}
}
@ -7284,6 +7533,27 @@ export const IDL: MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "padding",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "name",
"type": {
@ -7293,10 +7563,6 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "oracle",
"type": "publicKey"
@ -7424,16 +7690,13 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "quoteTokenIndex",
"type": "u16"
"name": "reserved",
"type": {
"array": [
"u8",
6
]
}
}
]
}
@ -7443,6 +7706,27 @@ export const IDL: MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "group",
"type": "publicKey"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "quoteTokenIndex",
"type": "u16"
},
{
"name": "padding",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "name",
"type": {
@ -7452,10 +7736,6 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "group",
"type": "publicKey"
},
{
"name": "serumProgram",
"type": "publicKey"
@ -7468,14 +7748,6 @@ export const IDL: MangoV4 = {
"name": "marketIndex",
"type": "u16"
},
{
"name": "baseTokenIndex",
"type": "u16"
},
{
"name": "quoteTokenIndex",
"type": "u16"
},
{
"name": "bump",
"type": "u8"
@ -7485,7 +7757,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
1
5
]
}
}

View File

@ -53,15 +53,16 @@ async function main() {
// group
console.log(`Creating Group...`);
const insuranceMint = new PublicKey(DEVNET_MINTS.get('USDC')!);
try {
await client.createGroup(0, true);
await client.createGroup(0, true, insuranceMint);
} catch (error) {
console.log(error);
}
const group = await client.getGroupForAdmin(admin.publicKey);
console.log(`...registered group ${group.publicKey}`);
// register token 0
// register token 1
console.log(`Registering BTC...`);
const btcDevnetMint = new PublicKey(DEVNET_MINTS.get('BTC')!);
const btcDevnetOracle = new PublicKey(DEVNET_ORACLES.get('BTC')!);
@ -71,7 +72,7 @@ async function main() {
btcDevnetMint,
btcDevnetOracle,
0.1,
0,
1, // tokenIndex
'BTC',
0.4,
0.07,
@ -91,7 +92,7 @@ async function main() {
console.log(error);
}
// stub oracle + register token 1
// stub oracle + register token 0
console.log(`Registering USDC...`);
const usdcDevnetMint = new PublicKey(DEVNET_MINTS.get('USDC')!);
try {
@ -109,7 +110,7 @@ async function main() {
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
1,
0, // tokenIndex
'USDC',
0.4,
0.07,
@ -228,7 +229,7 @@ async function main() {
0,
'BTC-PERP',
0.1,
0,
1,
6,
1,
10,
@ -251,7 +252,6 @@ async function main() {
const perpMarkets = await client.perpGetMarkets(
group,
group.banksMap.get('BTC')?.tokenIndex,
group.banksMap.get('USDC')?.tokenIndex,
);
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
@ -319,7 +319,7 @@ async function main() {
'BTC-PERP',
btcDevnetOracle,
0.2,
1,
0,
6,
0.9,
0.9,
@ -345,7 +345,7 @@ async function main() {
'BTC-PERP',
btcDevnetOracle,
0.1,
0,
1,
6,
1,
0.95,

View File

@ -67,12 +67,12 @@ async function main() {
const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
);
await client.editMangoAccount(group, 'my_changed_name', randomKey);
await client.editMangoAccount(group, mangoAccount, 'my_changed_name', randomKey);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
console.log(`...resetting mango account name, and re-setting a delegate`);
await client.editMangoAccount(group, 'my_mango_account', PublicKey.default);
await client.editMangoAccount(group, mangoAccount, 'my_mango_account', PublicKey.default);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
}

View File

@ -12,4 +12,4 @@ anchor build --skip-lint
# update types in ts client package
cp -v ./target/types/mango_v4.ts ./ts/client/src/mango_v4.ts
(cd ./ts/client && tsc)
(cd ./ts/client && yarn tsc)