mango/program/src/processor.rs

1931 lines
74 KiB
Rust

use std::cmp;
use std::cmp::min;
use std::mem::size_of;
use arrayref::{array_ref, array_refs};
use fixed::types::U64F64;
use fixed_macro::types::U64F64;
use flux_aggregator::borsh_state::InitBorshState;
use serum_dex::matching::Side;
use serum_dex::state::ToAlignedBytes;
use solana_program::account_info::AccountInfo;
use solana_program::clock::Clock;
use solana_program::entrypoint::ProgramResult;
use solana_program::instruction::{AccountMeta, Instruction};
use solana_program::msg;
use solana_program::program_error::ProgramError;
use solana_program::program_pack::{IsInitialized, Pack};
use solana_program::pubkey::Pubkey;
use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;
use spl_token::state::{Account, Mint};
use crate::error::{check_assert, MangoError, MangoErrorCode, MangoResult, SourceFileId};
use crate::instruction::MangoInstruction;
use crate::state::{AccountFlag, check_open_orders, DUST_THRESHOLD, load_asks_mut, load_bids_mut, load_market_state, load_open_orders, Loadable, MangoGroup, MangoIndex, MangoSrmAccount, MarginAccount, NUM_MARKETS, NUM_TOKENS, ONE_U64F64, PARTIAL_LIQ_INCENTIVE, ZERO_U64F64, INFO_LEN};
use crate::utils::{gen_signer_key, gen_signer_seeds};
macro_rules! check_default {
($cond:expr) => {
check_assert($cond, MangoErrorCode::Default, line!(), SourceFileId::Processor)
}
}
macro_rules! check_eq_default {
($x:expr, $y:expr) => {
check_assert($x == $y, MangoErrorCode::Default, line!(), SourceFileId::Processor)
}
}
macro_rules! check {
($cond:expr, $err:expr) => {
check_assert($cond, $err, line!(), SourceFileId::Processor)
}
}
macro_rules! check_eq {
($x:expr, $y:expr, $err:expr) => {
check_assert($x == $y, $err, line!(), SourceFileId::Processor)
}
}
macro_rules! throw_err {
($err:expr) => {
Err(MangoError::MangoErrorCode { mango_error_code: $err, line: line!(), source_file_id: SourceFileId::Processor })
}
}
pub mod srm_token {
use solana_program::declare_id;
#[cfg(feature = "devnet")]
declare_id!("9FbAMDvXqNjPqZSYt4EWTguJuDrGkfvwr3gSFpiSbX9S");
#[cfg(not(feature = "devnet"))]
declare_id!("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt");
}
pub const LIQ_MIN_COLL_RATIO: U64F64 = U64F64!(1.01);
pub struct Processor {}
impl Processor {
#[inline(never)]
fn init_mango_group(
program_id: &Pubkey,
accounts: &[AccountInfo],
signer_nonce: u64,
maint_coll_ratio: U64F64,
init_coll_ratio: U64F64,
borrow_limits: [u64; NUM_TOKENS]
) -> MangoResult<()> {
const NUM_FIXED: usize = 7;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_TOKENS + 2 * NUM_MARKETS];
let (
fixed_accs,
token_mint_accs,
vault_accs,
spot_market_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_TOKENS, NUM_TOKENS, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
rent_acc,
clock_acc,
signer_acc,
dex_prog_acc,
srm_vault_acc,
admin_acc
] = fixed_accs;
// Note: no need to check rent and clock because they're being checked in from_account_info
let rent = Rent::from_account_info(rent_acc)?;
let clock = Clock::from_account_info(clock_acc)?;
// TODO this may not be necessary since load_mut maps the data and will fail if size incorrect
check_eq!(size_of::<MangoGroup>(), mango_group_acc.data_len(), MangoErrorCode::InvalidMangoGroupSize)?;
let mut mango_group = MangoGroup::load_mut(mango_group_acc)?;
check_eq!(mango_group_acc.owner, program_id, MangoErrorCode::InvalidGroupOwner)?;
check_eq!(mango_group.account_flags, 0, MangoErrorCode::InvalidGroupFlags)?;
mango_group.account_flags = (AccountFlag::Initialized | AccountFlag::MangoGroup).bits();
check!(rent.is_exempt(mango_group_acc.lamports(), size_of::<MangoGroup>()), MangoErrorCode::GroupNotRentExempt)?;
check!(gen_signer_key(signer_nonce, mango_group_acc.key, program_id)? == *signer_acc.key, MangoErrorCode::InvalidSignerKey)?;
mango_group.signer_nonce = signer_nonce;
mango_group.signer_key = *signer_acc.key;
mango_group.dex_program_id = *dex_prog_acc.key;
mango_group.maint_coll_ratio = maint_coll_ratio;
mango_group.init_coll_ratio = init_coll_ratio;
// verify SRM vault is valid then set
let srm_vault = Account::unpack(&srm_vault_acc.try_borrow_data()?)?;
check!(srm_vault.is_initialized(), MangoErrorCode::Default)?;
check_eq!(&srm_vault.owner, signer_acc.key, MangoErrorCode::Default)?;
check_eq!(srm_token::ID, srm_vault.mint, MangoErrorCode::Default)?;
check_eq!(srm_vault_acc.owner, &spl_token::id(), MangoErrorCode::Default)?;
mango_group.srm_vault = *srm_vault_acc.key;
// Set the admin key and make sure it's a signer
check!(admin_acc.is_signer, MangoErrorCode::Default)?;
mango_group.admin = *admin_acc.key;
mango_group.borrow_limits = borrow_limits;
let curr_ts = clock.unix_timestamp as u64;
for i in 0..NUM_TOKENS {
let mint_acc = &token_mint_accs[i];
let mint = Mint::unpack(&mint_acc.try_borrow_data()?)?;
let vault_acc = &vault_accs[i];
let vault = Account::unpack(&vault_acc.try_borrow_data()?)?;
check!(vault.is_initialized(), MangoErrorCode::Default)?;
check_eq!(&vault.owner, signer_acc.key, MangoErrorCode::Default)?;
check_eq!(&vault.mint, mint_acc.key, MangoErrorCode::Default)?;
check_eq!(vault_acc.owner, &spl_token::id(), MangoErrorCode::Default)?;
mango_group.tokens[i] = *mint_acc.key;
mango_group.vaults[i] = *vault_acc.key;
mango_group.indexes[i] = MangoIndex {
last_update: curr_ts,
borrow: ONE_U64F64,
deposit: ONE_U64F64 // Smallest unit of interest is 0.0001% or 0.000001
};
mango_group.mint_decimals[i] = mint.decimals;
}
for i in 0..NUM_MARKETS {
let spot_market_acc: &AccountInfo = &spot_market_accs[i];
let spot_market = load_market_state(
spot_market_acc, dex_prog_acc.key
)?;
let sm_base_mint = spot_market.coin_mint;
let sm_quote_mint = spot_market.pc_mint;
check_eq!(sm_base_mint, token_mint_accs[i].key.to_aligned_bytes(), MangoErrorCode::Default)?;
check_eq!(sm_quote_mint, token_mint_accs[NUM_MARKETS].key.to_aligned_bytes(), MangoErrorCode::Default)?;
mango_group.spot_markets[i] = *spot_market_acc.key;
mango_group.oracles[i] = *oracle_accs[i].key;
let oracle = flux_aggregator::state::Aggregator::load_initialized(&oracle_accs[i])?;
mango_group.oracle_decimals[i] = oracle.config.decimals;
}
Ok(())
}
#[inline(never)]
fn init_margin_account(
program_id: &Pubkey,
accounts: &[AccountInfo]
) -> MangoResult<()> {
const NUM_FIXED: usize = 4;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
margin_account_acc,
owner_acc,
rent_acc
] = accounts;
let _mango_group = MangoGroup::load_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut(margin_account_acc)?;
let rent = Rent::from_account_info(rent_acc)?;
check_eq_default!(margin_account_acc.owner, program_id)?;
check_default!(rent.is_exempt(margin_account_acc.lamports(), size_of::<MarginAccount>()))?;
check_eq_default!(margin_account.account_flags, 0)?;
check_default!(owner_acc.is_signer)?;
margin_account.account_flags = (AccountFlag::Initialized | AccountFlag::MarginAccount).bits();
margin_account.mango_group = *mango_group_acc.key;
margin_account.owner = *owner_acc.key;
Ok(())
}
#[inline(never)]
fn deposit(
program_id: &Pubkey,
accounts: &[AccountInfo],
quantity: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 7;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
margin_account_acc,
owner_acc,
token_account_acc,
vault_acc,
token_prog_acc,
clock_acc,
] = accounts;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
check_eq!(&margin_account.owner, owner_acc.key, MangoErrorCode::InvalidMarginAccountOwner)?;
let token_index = mango_group.get_token_index_with_vault(vault_acc.key).unwrap();
check_eq_default!(&mango_group.vaults[token_index], vault_acc.key)?;
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
let deposit_instruction = spl_token::instruction::transfer(
&spl_token::id(),
token_account_acc.key,
vault_acc.key,
&owner_acc.key, &[], quantity
)?;
let deposit_accs = [
token_account_acc.clone(),
vault_acc.clone(),
owner_acc.clone(),
token_prog_acc.clone()
];
solana_program::program::invoke_signed(&deposit_instruction, &deposit_accs, &[])?;
let deposit: U64F64 = U64F64::from_num(quantity) / mango_group.indexes[token_index].deposit;
checked_add_deposit(&mut mango_group, &mut margin_account, token_index, deposit)?;
Ok(())
}
#[inline(never)]
fn withdraw(
program_id: &Pubkey,
accounts: &[AccountInfo],
quantity: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 8;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
margin_account_acc,
owner_acc,
token_account_acc,
vault_acc,
signer_acc,
token_prog_acc,
clock_acc,
] = fixed_accs;
let mut mango_group = MangoGroup::load_mut_checked(
mango_group_acc, program_id
)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&margin_account.owner, owner_acc.key)?;
for i in 0..NUM_MARKETS {
check_eq_default!(open_orders_accs[i].key, &margin_account.open_orders[i])?;
check_open_orders(&open_orders_accs[i], signer_acc.key)?;
}
let token_index = mango_group.get_token_index_with_vault(vault_acc.key).unwrap();
check_eq_default!(&mango_group.vaults[token_index], vault_acc.key)?;
let index: &MangoIndex = &mango_group.indexes[token_index];
let native_deposits: u64 = (margin_account.deposits[token_index].checked_mul(index.deposit).unwrap()).to_num();
let available = native_deposits;
check!(available >= quantity, MangoErrorCode::InsufficientFunds)?;
// TODO just borrow (quantity - available)
let prices = get_prices(&mango_group, oracle_accs)?;
// Withdraw from deposit
let withdrew: U64F64 = U64F64::from_num(quantity) / index.deposit;
checked_sub_deposit(&mut mango_group, &mut margin_account, token_index, withdrew)?;
// Make sure accounts are in valid state after withdrawal
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
check!(coll_ratio >= mango_group.init_coll_ratio, MangoErrorCode::CollateralRatioLimit)?;
check_default!(mango_group.has_valid_deposits_borrows(token_index))?;
// Send out withdraw instruction to SPL token program
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
let withdraw_instruction = spl_token::instruction::transfer(
&spl_token::ID,
vault_acc.key,
token_account_acc.key,
signer_acc.key,
&[],
quantity
)?;
let withdraw_accs = [
vault_acc.clone(),
token_account_acc.clone(),
signer_acc.clone(),
token_prog_acc.clone()
];
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
solana_program::program::invoke_signed(&withdraw_instruction, &withdraw_accs, &[&signer_seeds])?;
Ok(())
}
#[inline(never)]
fn borrow(
program_id: &Pubkey,
accounts: &[AccountInfo],
token_index: usize,
quantity: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 4;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
margin_account_acc,
owner_acc,
clock_acc,
] = fixed_accs;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key
)?;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&margin_account.owner, owner_acc.key)?;
for i in 0..NUM_MARKETS {
check_eq_default!(open_orders_accs[i].key, &margin_account.open_orders[i])?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let index: MangoIndex = mango_group.indexes[token_index];
let borrow = U64F64::from_num(quantity) / index.borrow;
let deposit = U64F64::from_num(quantity) / index.deposit;
checked_add_deposit(&mut mango_group, &mut margin_account, token_index, deposit)?;
checked_add_borrow(&mut mango_group, &mut margin_account, token_index, borrow)?;
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
check_default!(coll_ratio >= mango_group.init_coll_ratio)?;
check_default!(mango_group.has_valid_deposits_borrows(token_index))?;
Ok(())
}
#[inline(never)]
fn settle_borrow(
program_id: &Pubkey,
accounts: &[AccountInfo],
token_index: usize,
quantity: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 4;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
margin_account_acc,
owner_acc,
clock_acc,
] = accounts;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&margin_account.owner, owner_acc.key)?;
settle_borrow_unchecked(&mut mango_group, &mut margin_account, token_index, quantity)?;
Ok(())
}
#[inline(never)]
fn liquidate(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_deposit_quantities: [u64; NUM_TOKENS]
) -> MangoResult<()> {
throw_err!(MangoErrorCode::Deprecated)
}
#[inline(never)]
fn deposit_srm(
program_id: &Pubkey,
accounts: &[AccountInfo],
quantity: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 8;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
mango_srm_account_acc,
owner_acc,
srm_account_acc,
vault_acc,
token_prog_acc,
clock_acc,
rent_acc,
] = accounts;
// prog_assert!(owner_acc.is_signer)?; // anyone can deposit, not just owner
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
// Check if SRM is part of the MangoGroup, if so throw err
check!(mango_group.get_token_index(&srm_token::ID).is_none(), MangoErrorCode::FeeDiscountFunctionality)?;
// if MangoSrmAccount is empty, initialize it
check_eq_default!(mango_srm_account_acc.data_len(), size_of::<MangoSrmAccount>())?;
let mut mango_srm_account = MangoSrmAccount::load_mut(mango_srm_account_acc)?;
check_eq_default!(mango_srm_account_acc.owner, program_id)?;
if mango_srm_account.account_flags == 0 {
let rent = Rent::from_account_info(rent_acc)?;
check_default!(rent.is_exempt(mango_srm_account_acc.lamports(), size_of::<MangoSrmAccount>()))?;
mango_srm_account.account_flags = (AccountFlag::Initialized | AccountFlag::MangoSrmAccount).bits();
mango_srm_account.mango_group = *mango_group_acc.key;
check_default!(owner_acc.is_signer)?; // this is not necessary but whatever
mango_srm_account.owner = *owner_acc.key;
} else {
check_eq_default!(mango_srm_account.account_flags, (AccountFlag::Initialized | AccountFlag::MangoSrmAccount).bits())?;
check_eq_default!(&mango_srm_account.mango_group, mango_group_acc.key)?;
}
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
check_eq_default!(vault_acc.key, &mango_group.srm_vault)?;
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
let deposit_instruction = spl_token::instruction::transfer(
&spl_token::id(),
srm_account_acc.key,
vault_acc.key,
&owner_acc.key, &[], quantity
)?;
let deposit_accs = [
srm_account_acc.clone(),
vault_acc.clone(),
owner_acc.clone(),
token_prog_acc.clone()
];
solana_program::program::invoke_signed(&deposit_instruction, &deposit_accs, &[])?;
mango_srm_account.amount = mango_srm_account.amount.checked_add(quantity).unwrap();
Ok(())
}
#[inline(never)]
fn withdraw_srm(
program_id: &Pubkey,
accounts: &[AccountInfo],
quantity: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 8;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
mango_srm_account_acc,
owner_acc,
srm_account_acc,
vault_acc,
signer_acc,
token_prog_acc,
clock_acc,
] = accounts;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
// Check if SRM is part of the MangoGroup, if so throw err
check!(mango_group.get_token_index(&srm_token::ID).is_none(), MangoErrorCode::FeeDiscountFunctionality)?;
let mut mango_srm_account = MangoSrmAccount::load_mut_checked(
program_id, mango_srm_account_acc, mango_group_acc.key)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&mango_srm_account.owner, owner_acc.key)?;
check_eq_default!(vault_acc.key, &mango_group.srm_vault)?;
check_default!(mango_srm_account.amount >= quantity)?;
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
// Send out withdraw instruction to SPL token program
let withdraw_instruction = spl_token::instruction::transfer(
&spl_token::id(),
vault_acc.key,
srm_account_acc.key,
signer_acc.key,
&[],
quantity
)?;
let withdraw_accs = [
vault_acc.clone(),
srm_account_acc.clone(),
signer_acc.clone(),
token_prog_acc.clone()
];
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
solana_program::program::invoke_signed(&withdraw_instruction, &withdraw_accs, &[&signer_seeds])?;
mango_srm_account.amount = mango_srm_account.amount.checked_sub(quantity).unwrap();
Ok(())
}
#[inline(never)]
fn change_borrow_limit(
program_id: &Pubkey,
accounts: &[AccountInfo],
token_index: usize,
borrow_limit: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 2;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
admin_acc,
] = accounts;
let mut mango_group = MangoGroup::load_mut_checked(
mango_group_acc,
program_id
)?;
check_eq_default!(admin_acc.key, &mango_group.admin)?;
check_default!(admin_acc.is_signer)?;
mango_group.borrow_limits[token_index] = borrow_limit;
Ok(())
}
#[inline(never)]
fn place_order(
program_id: &Pubkey,
accounts: &[AccountInfo],
order: serum_dex::instruction::NewOrderInstructionV3
) -> MangoResult<()> {
const NUM_FIXED: usize = 17;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
owner_acc,
margin_account_acc,
clock_acc,
dex_prog_acc,
spot_market_acc,
dex_request_queue_acc,
dex_event_queue_acc,
bids_acc,
asks_acc,
vault_acc,
signer_acc,
dex_base_acc,
dex_quote_acc,
token_prog_acc,
rent_acc,
srm_vault_acc,
] = fixed_accs;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
if margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
margin_account.being_liquidated = false;
} else {
throw_err!(MangoErrorCode::BeingLiquidated)?;
}
}
let reduce_only = coll_ratio < mango_group.init_coll_ratio;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&margin_account.owner, owner_acc.key)?;
let market_i = mango_group.get_market_index(spot_market_acc.key).unwrap();
let token_i = match order.side {
Side::Bid => NUM_MARKETS,
Side::Ask => market_i
};
check_eq_default!(&mango_group.vaults[token_i], vault_acc.key)?;
let pre_amount = { // this is to keep track of how much funds were transferred out
let vault = Account::unpack(&vault_acc.try_borrow_data()?)?;
vault.amount
};
for i in 0..NUM_MARKETS {
let open_orders_acc = &open_orders_accs[i];
if i == market_i { // this one must not be default pubkey
check_default!(*open_orders_acc.key != Pubkey::default())?;
if margin_account.open_orders[i] == Pubkey::default() {
let open_orders = load_open_orders(open_orders_acc)?;
check_eq_default!(open_orders.account_flags, 0)?;
margin_account.open_orders[i] = *open_orders_acc.key;
}
} else {
check_eq_default!(open_orders_accs[i].key, &margin_account.open_orders[i])?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
}
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
check_eq_default!(dex_prog_acc.key, &mango_group.dex_program_id)?;
let data = serum_dex::instruction::MarketInstruction::NewOrderV3(order).pack();
let instruction = Instruction {
program_id: *dex_prog_acc.key,
data,
accounts: vec![
AccountMeta::new(*spot_market_acc.key, false),
AccountMeta::new(*open_orders_accs[market_i].key, false),
AccountMeta::new(*dex_request_queue_acc.key, false),
AccountMeta::new(*dex_event_queue_acc.key, false),
AccountMeta::new(*bids_acc.key, false),
AccountMeta::new(*asks_acc.key, false),
AccountMeta::new(*vault_acc.key, false),
AccountMeta::new_readonly(*signer_acc.key, true),
AccountMeta::new(*dex_base_acc.key, false),
AccountMeta::new(*dex_quote_acc.key, false),
AccountMeta::new_readonly(*token_prog_acc.key, false),
AccountMeta::new_readonly(*rent_acc.key, false),
AccountMeta::new(*srm_vault_acc.key, false),
],
};
let account_infos = [
dex_prog_acc.clone(), // Have to add account of the program id
spot_market_acc.clone(),
open_orders_accs[market_i].clone(),
dex_request_queue_acc.clone(),
dex_event_queue_acc.clone(),
bids_acc.clone(),
asks_acc.clone(),
vault_acc.clone(),
signer_acc.clone(),
dex_base_acc.clone(),
dex_quote_acc.clone(),
token_prog_acc.clone(),
rent_acc.clone(),
srm_vault_acc.clone(),
];
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
solana_program::program::invoke_signed(&instruction, &account_infos, &[&signer_seeds])?;
let post_amount = {
let vault = Account::unpack(&vault_acc.try_borrow_data()?)?;
vault.amount
};
let spent = pre_amount.checked_sub(post_amount).unwrap();
let index: MangoIndex = mango_group.indexes[token_i];
let native_deposit = margin_account.get_native_deposit(&index, token_i);
// user deposits will be used first.
// If user does not want that to happen, they must first issue a borrow command
if native_deposit >= spent {
let spent_deposit = U64F64::from_num(spent) / index.deposit;
checked_sub_deposit(&mut mango_group, &mut margin_account, token_i, spent_deposit)?;
} else {
let avail_deposit = margin_account.deposits[token_i];
checked_sub_deposit(&mut mango_group, &mut margin_account, token_i, avail_deposit)?;
let rem_spend = U64F64::from_num(spent - native_deposit);
check_default!(!reduce_only)?; // Cannot borrow more in reduce only mode
checked_add_borrow(&mut mango_group, &mut margin_account, token_i , rem_spend / index.borrow)?;
}
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
check_default!(reduce_only || coll_ratio >= mango_group.init_coll_ratio)?;
check_default!(mango_group.has_valid_deposits_borrows(token_i))?;
Ok(())
}
#[inline(never)]
fn settle_funds(
program_id: &Pubkey,
accounts: &[AccountInfo]
) -> MangoResult<()> {
const NUM_FIXED: usize = 14;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
owner_acc, // signer
margin_account_acc,
clock_acc,
dex_prog_acc,
spot_market_acc,
open_orders_acc,
signer_acc,
dex_base_acc,
dex_quote_acc,
base_vault_acc,
quote_vault_acc,
dex_signer_acc,
token_prog_acc,
] = accounts;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id,
margin_account_acc,
mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let market_i = mango_group.get_market_index(spot_market_acc.key).unwrap();
check_default!(owner_acc.is_signer)?;
check_eq_default!(owner_acc.key, &margin_account.owner)?;
check_eq_default!(&margin_account.open_orders[market_i], open_orders_acc.key)?;
check_eq_default!(base_vault_acc.key, &mango_group.vaults[market_i])?;
check_eq_default!(quote_vault_acc.key, &mango_group.vaults[NUM_MARKETS])?;
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
check_eq_default!(dex_prog_acc.key, &mango_group.dex_program_id)?;
if *open_orders_acc.key == Pubkey::default() {
return Ok(());
}
let (pre_base, pre_quote) = {
let open_orders = load_open_orders(open_orders_acc)?;
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
};
if pre_base == 0 && pre_quote == 0 {
return Ok(());
}
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
invoke_settle_funds(
dex_prog_acc,
spot_market_acc,
open_orders_acc,
signer_acc,
dex_base_acc,
dex_quote_acc,
base_vault_acc,
quote_vault_acc,
dex_signer_acc,
token_prog_acc,
&[&signer_seeds]
)?;
let (post_base, post_quote) = {
let open_orders = load_open_orders(open_orders_acc)?;
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
};
check_default!(post_base <= pre_base)?;
check_default!(post_quote <= pre_quote)?;
let base_change = U64F64::from_num(pre_base - post_base) / mango_group.indexes[market_i].deposit;
let quote_change = U64F64::from_num(pre_quote - post_quote) / mango_group.indexes[NUM_MARKETS].deposit;
checked_add_deposit(&mut mango_group, &mut margin_account, market_i, base_change)?;
checked_add_deposit(&mut mango_group, &mut margin_account, NUM_MARKETS, quote_change)?;
Ok(())
}
#[inline(never)]
fn cancel_order(
program_id: &Pubkey,
accounts: &[AccountInfo],
data: Vec<u8>
) -> MangoResult<()> {
const NUM_FIXED: usize = 11;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
owner_acc, // signer
margin_account_acc,
clock_acc,
dex_prog_acc,
spot_market_acc,
bids_acc,
asks_acc,
open_orders_acc,
signer_acc,
dex_event_queue_acc,
] = accounts;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let margin_account = MarginAccount::load_checked(
program_id,
margin_account_acc,
mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
check_eq_default!(dex_prog_acc.key, &mango_group.dex_program_id)?;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&margin_account.owner, owner_acc.key)?;
let market_i = mango_group.get_market_index(spot_market_acc.key).unwrap();
check_eq_default!(&margin_account.open_orders[market_i], open_orders_acc.key)?;
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
invoke_cancel_order(
dex_prog_acc,
spot_market_acc,
bids_acc,
asks_acc,
open_orders_acc,
signer_acc,
dex_event_queue_acc,
data,
&[&signer_seeds]
)?;
Ok(())
}
#[inline(never)]
fn place_and_settle(
program_id: &Pubkey,
accounts: &[AccountInfo],
order: serum_dex::instruction::NewOrderInstructionV3
) -> MangoResult<()> {
const NUM_FIXED: usize = 19;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
owner_acc,
margin_account_acc,
clock_acc,
dex_prog_acc,
spot_market_acc,
dex_request_queue_acc,
dex_event_queue_acc,
bids_acc,
asks_acc,
base_vault_acc,
quote_vault_acc,
signer_acc,
dex_base_acc,
dex_quote_acc,
token_prog_acc,
rent_acc,
srm_vault_acc,
dex_signer_acc
] = fixed_accs;
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key
)?;
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
if margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
margin_account.being_liquidated = false;
} else {
throw_err!(MangoErrorCode::BeingLiquidated)?;
}
}
let reduce_only = coll_ratio < mango_group.init_coll_ratio;
check_default!(owner_acc.is_signer)?;
check_eq_default!(&margin_account.owner, owner_acc.key)?;
let market_i = mango_group.get_market_index(spot_market_acc.key).unwrap();
let side = order.side;
let (in_token_i, out_token_i, vault_acc) = match side {
Side::Bid => (market_i, NUM_MARKETS, quote_vault_acc),
Side::Ask => (NUM_MARKETS, market_i, base_vault_acc)
};
check_eq_default!(&mango_group.vaults[market_i], base_vault_acc.key)?;
check_eq_default!(&mango_group.vaults[NUM_MARKETS], quote_vault_acc.key)?;
let (pre_base, pre_quote) = {
(Account::unpack(&base_vault_acc.try_borrow_data()?)?.amount,
Account::unpack(&quote_vault_acc.try_borrow_data()?)?.amount)
};
for i in 0..NUM_MARKETS {
let open_orders_acc = &open_orders_accs[i];
if i == market_i { // this one must not be default pubkey
check_default!(*open_orders_acc.key != Pubkey::default())?;
// if this is first time using this open_orders_acc, check and save it
if margin_account.open_orders[i] == Pubkey::default() {
let open_orders = load_open_orders(open_orders_acc)?;
check_eq_default!(open_orders.account_flags, 0)?;
margin_account.open_orders[i] = *open_orders_acc.key;
} else {
check_eq_default!(open_orders_accs[i].key, &margin_account.open_orders[i])?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
} else {
check_eq_default!(open_orders_accs[i].key, &margin_account.open_orders[i])?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
}
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
check_eq_default!(dex_prog_acc.key, &mango_group.dex_program_id)?;
let data = serum_dex::instruction::MarketInstruction::NewOrderV3(order).pack();
let instruction = Instruction {
program_id: *dex_prog_acc.key,
data,
accounts: vec![
AccountMeta::new(*spot_market_acc.key, false),
AccountMeta::new(*open_orders_accs[market_i].key, false),
AccountMeta::new(*dex_request_queue_acc.key, false),
AccountMeta::new(*dex_event_queue_acc.key, false),
AccountMeta::new(*bids_acc.key, false),
AccountMeta::new(*asks_acc.key, false),
AccountMeta::new(*vault_acc.key, false),
AccountMeta::new_readonly(*signer_acc.key, true),
AccountMeta::new(*dex_base_acc.key, false),
AccountMeta::new(*dex_quote_acc.key, false),
AccountMeta::new_readonly(*token_prog_acc.key, false),
AccountMeta::new_readonly(*rent_acc.key, false),
AccountMeta::new(*srm_vault_acc.key, false),
],
};
let account_infos = [
dex_prog_acc.clone(), // Have to add account of the program id
spot_market_acc.clone(),
open_orders_accs[market_i].clone(),
dex_request_queue_acc.clone(),
dex_event_queue_acc.clone(),
bids_acc.clone(),
asks_acc.clone(),
vault_acc.clone(),
signer_acc.clone(),
dex_base_acc.clone(),
dex_quote_acc.clone(),
token_prog_acc.clone(),
rent_acc.clone(),
srm_vault_acc.clone(),
];
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
solana_program::program::invoke_signed(&instruction, &account_infos, &[&signer_seeds])?;
// Settle funds for this market
invoke_settle_funds(
dex_prog_acc,
spot_market_acc,
&open_orders_accs[market_i],
signer_acc,
dex_base_acc,
dex_quote_acc,
base_vault_acc,
quote_vault_acc,
dex_signer_acc,
token_prog_acc,
&[&signer_seeds]
)?;
let (post_base, post_quote) = {
(Account::unpack(&base_vault_acc.try_borrow_data()?)?.amount,
Account::unpack(&quote_vault_acc.try_borrow_data()?)?.amount)
};
let (pre_in, pre_out, post_in, post_out) = match side {
Side::Bid => (pre_base, pre_quote, post_base, post_quote),
Side::Ask => (pre_quote, pre_base, post_quote, post_base)
};
// It's possible the net change was positive for both tokens
// It's not possible for in_token to be negative
let out_index: MangoIndex = mango_group.indexes[out_token_i];
let in_index: MangoIndex = mango_group.indexes[in_token_i];
// if out token was net negative, then you may need to borrow more
if post_out < pre_out {
let total_out = pre_out.checked_sub(post_out).unwrap();
let native_deposit = margin_account.get_native_deposit(&out_index, out_token_i);
if native_deposit < total_out { // need to borrow
let avail_deposit = margin_account.deposits[out_token_i];
checked_sub_deposit(&mut mango_group, &mut margin_account, out_token_i, avail_deposit)?;
let rem_spend = U64F64::from_num(total_out - native_deposit);
check_default!(!reduce_only)?; // Cannot borrow more in reduce only mode
checked_add_borrow(&mut mango_group, &mut margin_account, out_token_i, rem_spend / out_index.borrow)?;
} else { // just spend user deposits
let mango_spent = U64F64::from_num(total_out) / out_index.deposit;
checked_sub_deposit(&mut mango_group, &mut margin_account, out_token_i, mango_spent)?;
}
} else { // Add out token deposit
let deposit = U64F64::from_num(post_out.checked_sub(pre_out).unwrap()) / out_index.deposit;
checked_add_deposit(&mut mango_group, &mut margin_account, out_token_i, deposit)?;
}
let total_in = U64F64::from_num(post_in.checked_sub(pre_in).unwrap()) / in_index.deposit;
checked_add_deposit(&mut mango_group, &mut margin_account, in_token_i, total_in)?;
// Settle borrow
// TODO only do ops on tokens that have borrows and deposits
settle_borrow_full_unchecked(&mut mango_group, &mut margin_account, out_token_i)?;
settle_borrow_full_unchecked(&mut mango_group, &mut margin_account, in_token_i)?;
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
check!(reduce_only || coll_ratio >= mango_group.init_coll_ratio, MangoErrorCode::CollateralRatioLimit)?;
check_default!(mango_group.has_valid_deposits_borrows(out_token_i))?;
Ok(())
}
/// Liquidator is allowed to cancel orders of an account that is being_liquidated
/// This will also settle funds
#[inline(never)]
fn force_cancel_orders(
program_id: &Pubkey,
accounts: &[AccountInfo],
limit: u8
) -> MangoResult<()> {
const NUM_FIXED: usize = 16;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
liqor_acc,
liqee_margin_account_acc,
base_vault_acc,
quote_vault_acc,
spot_market_acc,
bids_acc,
asks_acc,
signer_acc,
dex_event_queue_acc,
dex_base_acc,
dex_quote_acc,
dex_signer_acc,
token_prog_acc,
dex_prog_acc,
clock_acc
] = fixed_accs;
check_eq!(token_prog_acc.key, &spl_token::id(), MangoErrorCode::InvalidProgramId)?;
check!(liqor_acc.is_signer, MangoErrorCode::SignerNecessary)?;
let mut mango_group = MangoGroup::load_mut_checked(
mango_group_acc, program_id
)?;
check_eq!(dex_prog_acc.key, &mango_group.dex_program_id, MangoErrorCode::InvalidProgramId)?;
check_eq!(signer_acc.key, &mango_group.signer_key, MangoErrorCode::InvalidSignerKey)?;
let market_i = mango_group.get_market_index(spot_market_acc.key).unwrap();
check_eq!(&mango_group.vaults[market_i], base_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
check_eq!(&mango_group.vaults[NUM_MARKETS], quote_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
check_eq_default!(spot_market_acc.key, &mango_group.spot_markets[market_i])?;
let mut liqee_margin_account = MarginAccount::load_mut_checked(
program_id, liqee_margin_account_acc, mango_group_acc.key
)?;
for i in 0..NUM_MARKETS {
check_eq!(open_orders_accs[i].key, &liqee_margin_account.open_orders[i],
MangoErrorCode::InvalidOpenOrdersAccount)?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = liqee_margin_account.get_collateral_ratio(
&mango_group, &prices, open_orders_accs)?;
// Only allow liquidations on accounts already being liquidated and below init or accounts below maint
if liqee_margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
liqee_margin_account.being_liquidated = false;
return Ok(());
}
} else if coll_ratio < mango_group.maint_coll_ratio {
liqee_margin_account.being_liquidated = true;
} else {
throw_err!(MangoErrorCode::NotLiquidatable)?;
}
let open_orders_acc = &open_orders_accs[market_i];
let signers_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
invoke_cancel_orders(open_orders_acc, dex_prog_acc, spot_market_acc, bids_acc, asks_acc, signer_acc,
dex_event_queue_acc, &[&signers_seeds], limit)?;
let (pre_base, pre_quote) = {
let open_orders = load_open_orders(open_orders_acc)?;
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
};
if pre_base == 0 && pre_quote == 0 {
return Ok(());
}
invoke_settle_funds(dex_prog_acc, spot_market_acc, open_orders_acc, signer_acc, dex_base_acc,
dex_quote_acc, base_vault_acc, quote_vault_acc, dex_signer_acc,
token_prog_acc, &[&signers_seeds])?;
let (post_base, post_quote) = {
let open_orders = load_open_orders(open_orders_acc)?;
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
};
check_default!(post_base <= pre_base)?;
check_default!(post_quote <= pre_quote)?;
let base_change = U64F64::from_num(pre_base - post_base) / mango_group.indexes[market_i].deposit;
let quote_change = U64F64::from_num(pre_quote - post_quote) / mango_group.indexes[NUM_MARKETS].deposit;
checked_add_deposit(&mut mango_group, &mut liqee_margin_account, market_i, base_change)?;
checked_add_deposit(&mut mango_group, &mut liqee_margin_account, NUM_MARKETS, quote_change)?;
Ok(())
}
#[inline(never)]
fn partial_liquidate(
program_id: &Pubkey,
accounts: &[AccountInfo],
max_deposit: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 10;
// TODO make it so canceling orders feature is optional if no orders outstanding to cancel
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
liqor_acc,
liqor_in_token_acc,
liqor_out_token_acc,
liqee_margin_account_acc,
in_vault_acc,
out_vault_acc,
signer_acc,
token_prog_acc,
clock_acc,
] = fixed_accs;
check!(token_prog_acc.key == &spl_token::ID, MangoErrorCode::InvalidProgramId)?;
check!(liqor_acc.is_signer, MangoErrorCode::SignerNecessary)?;
let mut mango_group = MangoGroup::load_mut_checked(
mango_group_acc, program_id
)?;
check_eq!(signer_acc.key, &mango_group.signer_key, MangoErrorCode::InvalidSignerKey)?;
let liqor_in_token_account = Account::unpack(&liqor_in_token_acc.try_borrow_data()?)?;
let in_token_index = mango_group.get_token_index(&liqor_in_token_account.mint).unwrap();
let liqor_out_token_account = Account::unpack(&liqor_out_token_acc.try_borrow_data()?)?;
let out_token_index = mango_group.get_token_index(&liqor_out_token_account.mint).unwrap();
check_default!(in_token_index != out_token_index)?;
check_eq!(&mango_group.vaults[in_token_index], in_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
check_eq!(&mango_group.vaults[out_token_index], out_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
let mut liqee_margin_account = MarginAccount::load_mut_checked(
program_id, liqee_margin_account_acc, mango_group_acc.key
)?;
for i in 0..NUM_MARKETS {
check_eq!(open_orders_accs[i].key, &liqee_margin_account.open_orders[i],
MangoErrorCode::InvalidOpenOrdersAccount)?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
// TODO - add a check to make sure indexes were updated in last hour
// if not updated, then update indexes and return without continuing
// there is not enough compute to continue
// code is written below but needs to be tested on devnet first
let clock = Clock::from_account_info(clock_acc)?;
let now_ts = clock.unix_timestamp as u64;
for i in 0..NUM_TOKENS {
if now_ts > mango_group.indexes[i].last_update + 3600 {
msg!("Invalid indexes");
mango_group.update_indexes(&clock)?;
return Ok(());
}
}
let prices = get_prices(&mango_group, oracle_accs)?;
let start_assets = liqee_margin_account.get_assets(&mango_group, open_orders_accs)?;
let start_liabs = liqee_margin_account.get_liabs(&mango_group)?;
let coll_ratio = liqee_margin_account.coll_ratio_from_assets_liabs(
&prices, &start_assets, &start_liabs)?;
// Only allow liquidations on accounts already being liquidated and below init or accounts below maint
if liqee_margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
liqee_margin_account.being_liquidated = false;
return Ok(());
}
} else if coll_ratio >= mango_group.maint_coll_ratio {
throw_err!(MangoErrorCode::NotLiquidatable)?;
}
// Settle borrows to increase coll ratio if possible
for i in 0..NUM_TOKENS {
settle_borrow_full_unchecked(&mut mango_group, &mut liqee_margin_account, i)?;
}
// Check again to see if account still liquidatable
let coll_ratio = liqee_margin_account.get_collateral_ratio(
&mango_group, &prices, open_orders_accs)?;
if liqee_margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
// TODO make sure liquidator knows why tx was success but he didn't receive any funds
msg!("Account above init_coll_ratio after settling borrows");
liqee_margin_account.being_liquidated = false;
return Ok(());
}
} else if coll_ratio >= mango_group.maint_coll_ratio {
msg!("Account above maint_coll_ratio after settling borrows");
return Ok(());
} else {
liqee_margin_account.being_liquidated = true;
}
// Get how much to deposit and how much to withdraw
let (in_quantity, out_quantity) = get_in_out_quantities(
&mut mango_group, &mut liqee_margin_account, open_orders_accs, &prices, in_token_index,
out_token_index, max_deposit
)?;
let signer_nonce = mango_group.signer_nonce;
let signers_seeds = gen_signer_seeds(&signer_nonce, mango_group_acc.key);
invoke_transfer(token_prog_acc, liqor_in_token_acc, in_vault_acc, liqor_acc,
&[&signers_seeds], in_quantity)?;
invoke_transfer(token_prog_acc, out_vault_acc, liqor_out_token_acc, signer_acc,
&[&signers_seeds], out_quantity)?;
// Check if account valid now
let end_assets = liqee_margin_account.get_assets(&mango_group, open_orders_accs)?;
let end_liabs = liqee_margin_account.get_liabs(&mango_group)?;
let coll_ratio = liqee_margin_account.coll_ratio_from_assets_liabs(
&prices, &end_assets, &end_liabs)?;
let mut total_deposits = [ZERO_U64F64; NUM_TOKENS];
let mut socialized_losses = false;
if coll_ratio >= mango_group.init_coll_ratio {
// set margin account to no longer being liquidated
liqee_margin_account.being_liquidated = false;
} else {
// if all asset vals is dust (less than 1 cent?) socialize loss on lenders
let assets_val = liqee_margin_account.get_assets_val(&mango_group, &prices, open_orders_accs)?;
if assets_val < DUST_THRESHOLD {
for i in 0..NUM_TOKENS {
let native_borrow: U64F64 = end_liabs[i];
let total_deposits_native: U64F64 = mango_group.total_deposits[i] * mango_group.indexes[i].deposit;
total_deposits[i] = total_deposits_native;
if native_borrow > 0 {
socialized_losses = true;
socialize_loss(
&mut mango_group,
&mut liqee_margin_account,
i,
native_borrow,
total_deposits_native
)?;
}
}
}
}
// Note total_deposits is only logged with reasonable values if assets_val < DUST_THRESHOLD
log_liquidation_details(&start_assets, &start_liabs, &end_assets, &end_liabs, &prices, socialized_losses, &total_deposits);
// TODO do I need to check total deposits and total borrows?
// TODO log deposit indexes before and after liquidation as a way to measure socialize of losses
Ok(())
}
#[inline(never)]
fn add_margin_account_info(
program_id: &Pubkey,
accounts: &[AccountInfo],
info: [u8; INFO_LEN]
) -> MangoResult<()> {
const NUM_FIXED: usize = 3;
let accounts = array_ref![accounts, 0, NUM_FIXED];
let [
mango_group_acc,
margin_account_acc,
owner_acc,
] = accounts;
let mut margin_account = MarginAccount::load_mut_checked(
program_id, margin_account_acc, mango_group_acc.key)?;
check_eq!(owner_acc.key, &margin_account.owner, MangoErrorCode::InvalidMarginAccountOwner)?;
check!(owner_acc.is_signer, MangoErrorCode::SignerNecessary)?;
margin_account.info = info;
Ok(())
}
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
data: &[u8]
) -> MangoResult<()> {
let instruction = MangoInstruction::unpack(data).ok_or(ProgramError::InvalidInstructionData)?;
match instruction {
MangoInstruction::InitMangoGroup {
signer_nonce, maint_coll_ratio, init_coll_ratio, borrow_limits
} => {
msg!("Mango: InitMangoGroup");
Self::init_mango_group(program_id, accounts, signer_nonce, maint_coll_ratio, init_coll_ratio, borrow_limits)?;
}
MangoInstruction::InitMarginAccount => {
msg!("Mango: InitMarginAccount");
Self::init_margin_account(program_id, accounts)?;
}
MangoInstruction::Deposit {
quantity
} => {
msg!("Mango: Deposit");
Self::deposit(program_id, accounts, quantity)?;
}
MangoInstruction::Withdraw {
quantity
} => {
msg!("Mango: Withdraw");
Self::withdraw(program_id, accounts, quantity)?;
}
MangoInstruction::Borrow {
token_index,
quantity
} => {
msg!("Mango: Borrow");
Self::borrow(program_id, accounts, token_index, quantity)?;
}
MangoInstruction::SettleBorrow {
token_index,
quantity
} => {
msg!("Mango: SettleBorrow");
Self::settle_borrow(program_id, accounts, token_index, quantity)?;
}
MangoInstruction::Liquidate {
deposit_quantities
} => {
// Either user takes the position
// Or the program can liquidate on the serum dex (in case no liquidator wants to take pos)
msg!("Mango: Liquidate");
Self::liquidate(program_id, accounts, deposit_quantities)?;
}
MangoInstruction::DepositSrm {
quantity
} => {
msg!("Mango: DepositSrm");
Self::deposit_srm(program_id, accounts, quantity)?;
}
MangoInstruction::WithdrawSrm {
quantity
} => {
msg!("Mango: WithdrawSrm");
Self::withdraw_srm(program_id, accounts, quantity)?;
}
MangoInstruction::PlaceOrder {
order
} => {
msg!("Mango: PlaceOrder");
Self::place_order(program_id, accounts, order)?;
}
MangoInstruction::SettleFunds => {
msg!("Mango: SettleFunds");
Self::settle_funds(program_id, accounts)?;
}
MangoInstruction::CancelOrder {
order
} => {
msg!("Mango: CancelOrder");
let data = serum_dex::instruction::MarketInstruction::CancelOrderV2(order).pack();
Self::cancel_order(program_id, accounts, data)?;
}
MangoInstruction::CancelOrderByClientId {
client_id
} => {
msg!("Mango: CancelOrderByClientId");
Self::cancel_order(program_id, accounts, client_id.to_le_bytes().to_vec())?;
}
MangoInstruction::ChangeBorrowLimit {
token_index, borrow_limit
} => {
msg!("Mango: ChangeBorrowLimit");
Self::change_borrow_limit(program_id, accounts, token_index, borrow_limit)?;
}
MangoInstruction::PlaceAndSettle {
order
} => {
msg!("Mango: PlaceAndSettle");
Self::place_and_settle(program_id, accounts, order)?;
}
MangoInstruction::ForceCancelOrders {
limit
} => {
msg!("Mango: ForceCancelOrders");
Self::force_cancel_orders(program_id, accounts, limit)?;
}
MangoInstruction::PartialLiquidate {
max_deposit
} => {
msg!("Mango: PartialLiquidate");
Self::partial_liquidate(program_id, accounts, max_deposit)?;
}
MangoInstruction::AddMarginAccountInfo {
info
} => {
msg!("Mango: AddMarginAccountInfo");
Self::add_margin_account_info(program_id, accounts, info)?;
}
}
Ok(())
}
}
fn log_liquidation_details(
start_assets: &[U64F64; NUM_TOKENS],
start_liabs: &[U64F64; NUM_TOKENS],
end_assets: &[U64F64; NUM_TOKENS],
end_liabs: &[U64F64; NUM_TOKENS],
prices: &[U64F64; NUM_TOKENS],
socialized_losses: bool,
total_deposits: &[U64F64; NUM_TOKENS]
) {
let mut prices_f64 = [0_f64; NUM_TOKENS];
let mut start_assets_u64 = [0u64; NUM_TOKENS];
let mut start_liabs_u64 = [0u64; NUM_TOKENS];
let mut end_assets_u64 = [0u64; NUM_TOKENS];
let mut end_liabs_u64 = [0u64; NUM_TOKENS];
let mut total_deposits_u64 = [0u64; NUM_TOKENS];
for i in 0..NUM_TOKENS {
prices_f64[i] = prices[i].to_num::<f64>();
start_assets_u64[i] = start_assets[i].to_num();
start_liabs_u64[i] = start_liabs[i].to_num();
end_assets_u64[i] = end_assets[i].to_num();
end_liabs_u64[i] = end_liabs[i].to_num();
total_deposits_u64[i] = total_deposits[i].to_num();
}
msg!("liquidation details: {{ \
\"start\": {{ \"assets\": {:?}, \"liabs\": {:?} }}, \
\"end\": {{ \"assets\": {:?}, \"liabs\": {:?} }}, \
\"prices\": {:?}, \
\"socialized_losses\": {}, \
\"total_deposits\": {:?} \
}}", start_assets_u64, start_liabs_u64, end_assets_u64, end_liabs_u64, prices_f64, socialized_losses, total_deposits_u64);
}
fn settle_borrow_unchecked(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
quantity: u64
) -> MangoResult<()> {
let deposit_index = mango_group.indexes[token_index].deposit;
let borrow_index = mango_group.indexes[token_index].borrow;
let native_borrow: U64F64 = margin_account.borrows[token_index] * borrow_index;
let native_deposit: U64F64 = margin_account.deposits[token_index] * deposit_index;
let quantity = U64F64::from_num(quantity);
let quantity = min(quantity, native_deposit);
if quantity >= native_borrow { // Reduce borrows to 0 to prevent rounding related dust
// NOTE: native_borrow / index.borrow is same as margin_account.borrows[token_index]
checked_sub_deposit(mango_group, margin_account, token_index, native_borrow / deposit_index)?;
checked_sub_borrow(mango_group, margin_account, token_index, margin_account.borrows[token_index])?;
} else {
checked_sub_deposit(mango_group, margin_account, token_index, quantity / deposit_index)?;
checked_sub_borrow(mango_group, margin_account, token_index, quantity / borrow_index)?;
}
// No need to check collateralization ratio or deposits/borrows validity
Ok(())
}
fn settle_borrow_full_unchecked(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
) -> MangoResult<()> {
let index: &MangoIndex = &mango_group.indexes[token_index];
let native_borrow = margin_account.get_native_borrow(index, token_index);
let native_deposit = margin_account.get_native_deposit(index, token_index);
let quantity = cmp::min(native_borrow, native_deposit);
let borr_settle = U64F64::from_num(quantity) / index.borrow;
let dep_settle = U64F64::from_num(quantity) / index.deposit;
checked_sub_deposit(mango_group, margin_account, token_index, dep_settle)?;
checked_sub_borrow(mango_group, margin_account, token_index, borr_settle)?;
// No need to check collateralization ratio or deposits/borrows validity
Ok(())
}
fn socialize_loss(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
reduce_quantity_native: U64F64,
total_deposits_native: U64F64
) -> MangoResult<()> {
// reduce borrow for this margin_account by appropriate amount
// decrease MangoIndex.deposit by appropriate amount
// TODO make sure there is enough funds to socialize losses
let quantity: U64F64 = reduce_quantity_native / mango_group.indexes[token_index].borrow;
checked_sub_borrow(mango_group, margin_account, token_index, quantity)?;
let percentage_loss = reduce_quantity_native.checked_div(total_deposits_native).unwrap();
let index: &mut MangoIndex = &mut mango_group.indexes[token_index];
index.deposit = index.deposit
.checked_sub(percentage_loss.checked_mul(index.deposit).unwrap()).unwrap();
Ok(())
}
fn checked_sub_deposit(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
quantity: U64F64
) -> MangoResult<()> {
margin_account.checked_sub_deposit(token_index, quantity)?;
mango_group.checked_sub_deposit(token_index, quantity)
}
fn checked_sub_borrow(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
quantity: U64F64
) -> MangoResult<()> {
margin_account.checked_sub_borrow(token_index, quantity)?;
mango_group.checked_sub_borrow(token_index, quantity)?;
let mut has_borrows = false;
for i in 0..NUM_TOKENS {
if margin_account.borrows[i] > 0 {
has_borrows = true;
}
}
margin_account.has_borrows = has_borrows;
Ok(())
}
fn checked_add_deposit(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
quantity: U64F64
) -> MangoResult<()> {
margin_account.checked_add_deposit(token_index, quantity)?;
mango_group.checked_add_deposit(token_index, quantity)
}
fn checked_add_borrow(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
token_index: usize,
quantity: U64F64
) -> MangoResult<()> {
margin_account.checked_add_borrow(token_index, quantity)?;
mango_group.checked_add_borrow(token_index, quantity)?;
if !margin_account.has_borrows && quantity > 0 {
margin_account.has_borrows = true;
}
Ok(())
}
pub fn get_prices(
mango_group: &MangoGroup,
oracle_accs: &[AccountInfo]
) -> MangoResult<[U64F64; NUM_TOKENS]> {
let mut prices = [ZERO_U64F64; NUM_TOKENS];
prices[NUM_MARKETS] = ONE_U64F64; // quote currency is 1
let quote_decimals: u8 = mango_group.mint_decimals[NUM_MARKETS];
for i in 0..NUM_MARKETS {
check_eq_default!(&mango_group.oracles[i], oracle_accs[i].key)?;
// TODO store this info in MangoGroup, first make sure it cannot be changed by solink
let quote_adj = U64F64::from_num(
10u64.pow(quote_decimals.checked_sub(mango_group.oracle_decimals[i]).unwrap() as u32)
);
let answer = flux_aggregator::read_median(&oracle_accs[i])?; // this is in USD cents
let value = U64F64::from_num(answer.median);
let base_adj = U64F64::from_num(10u64.pow(mango_group.mint_decimals[i] as u32));
prices[i] = quote_adj
.checked_div(base_adj).unwrap()
.checked_mul(value).unwrap();
}
Ok(prices)
}
fn invoke_settle_funds<'a>(
dex_prog_acc: &AccountInfo<'a>,
spot_market_acc: &AccountInfo<'a>,
open_orders_acc: &AccountInfo<'a>,
signer_acc: &AccountInfo<'a>,
dex_base_acc: &AccountInfo<'a>,
dex_quote_acc: &AccountInfo<'a>,
base_vault_acc: &AccountInfo<'a>,
quote_vault_acc: &AccountInfo<'a>,
dex_signer_acc: &AccountInfo<'a>,
token_prog_acc: &AccountInfo<'a>,
signers_seeds: &[&[&[u8]]]
) -> ProgramResult {
let data = serum_dex::instruction::MarketInstruction::SettleFunds.pack();
let instruction = Instruction {
program_id: *dex_prog_acc.key,
data,
accounts: vec![
AccountMeta::new(*spot_market_acc.key, false),
AccountMeta::new(*open_orders_acc.key, false),
AccountMeta::new_readonly(*signer_acc.key, true),
AccountMeta::new(*dex_base_acc.key, false),
AccountMeta::new(*dex_quote_acc.key, false),
AccountMeta::new(*base_vault_acc.key, false),
AccountMeta::new(*quote_vault_acc.key, false),
AccountMeta::new_readonly(*dex_signer_acc.key, false),
AccountMeta::new_readonly(*token_prog_acc.key, false),
AccountMeta::new(*quote_vault_acc.key, false),
],
};
let account_infos = [
dex_prog_acc.clone(),
spot_market_acc.clone(),
open_orders_acc.clone(),
signer_acc.clone(),
dex_base_acc.clone(),
dex_quote_acc.clone(),
base_vault_acc.clone(),
quote_vault_acc.clone(),
dex_signer_acc.clone(),
token_prog_acc.clone()
];
solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds)
}
fn invoke_cancel_order<'a>(
dex_prog_acc: &AccountInfo<'a>,
spot_market_acc: &AccountInfo<'a>,
bids_acc: &AccountInfo<'a>,
asks_acc: &AccountInfo<'a>,
open_orders_acc: &AccountInfo<'a>,
signer_acc: &AccountInfo<'a>,
dex_event_queue_acc: &AccountInfo<'a>,
data: Vec<u8>,
signers_seeds: &[&[&[u8]]]
) -> ProgramResult {
let instruction = Instruction {
program_id: *dex_prog_acc.key,
data,
accounts: vec![
AccountMeta::new(*spot_market_acc.key, false),
AccountMeta::new(*bids_acc.key, false),
AccountMeta::new(*asks_acc.key, false),
AccountMeta::new(*open_orders_acc.key, false),
AccountMeta::new_readonly(*signer_acc.key, true),
AccountMeta::new(*dex_event_queue_acc.key, false),
],
};
let account_infos = [
dex_prog_acc.clone(),
spot_market_acc.clone(),
bids_acc.clone(),
asks_acc.clone(),
open_orders_acc.clone(),
signer_acc.clone(),
dex_event_queue_acc.clone()
];
solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds)
}
fn invoke_cancel_orders<'a>(
open_orders_acc: &AccountInfo<'a>,
dex_prog_acc: &AccountInfo<'a>,
spot_market_acc: &AccountInfo<'a>,
bids_acc: &AccountInfo<'a>,
asks_acc: &AccountInfo<'a>,
signer_acc: &AccountInfo<'a>,
dex_event_queue_acc: &AccountInfo<'a>,
signers_seeds: &[&[&[u8]]],
mut limit: u8
) -> MangoResult<()> {
let mut cancels = vec![];
{
let open_orders = load_open_orders(open_orders_acc)?;
let market = load_market_state(spot_market_acc, dex_prog_acc.key)?;
let bids = load_bids_mut(&market, bids_acc)?;
let asks = load_asks_mut(&market, asks_acc)?;
limit = min(limit, open_orders.free_slot_bits.count_zeros() as u8);
if limit == 0 {
return Ok(());
}
for j in 0..128 {
let slot_mask = 1u128 << j;
if open_orders.free_slot_bits & slot_mask != 0 { // means slot is free
continue;
}
let order_id = open_orders.orders[j];
let side = if open_orders.is_bid_bits & slot_mask != 0 {
match bids.find_by_key(order_id) {
None => { continue }
Some(_) => serum_dex::matching::Side::Bid
}
} else {
match asks.find_by_key(order_id) {
None => { continue }
Some(_) => serum_dex::matching::Side::Ask
}
};
let cancel_instruction = serum_dex::instruction::CancelOrderInstructionV2 { side, order_id };
cancels.push(cancel_instruction);
limit -= 1;
if limit == 0 {
break;
}
}
}
let mut instruction = Instruction {
program_id: *dex_prog_acc.key,
data: vec![],
accounts: vec![
AccountMeta::new(*spot_market_acc.key, false),
AccountMeta::new(*bids_acc.key, false),
AccountMeta::new(*asks_acc.key, false),
AccountMeta::new(*open_orders_acc.key, false),
AccountMeta::new_readonly(*signer_acc.key, true),
AccountMeta::new(*dex_event_queue_acc.key, false),
],
};
let account_infos = [
dex_prog_acc.clone(),
spot_market_acc.clone(),
bids_acc.clone(),
asks_acc.clone(),
open_orders_acc.clone(),
signer_acc.clone(),
dex_event_queue_acc.clone()
];
for cancel in cancels.iter() {
let cancel_instruction = serum_dex::instruction::MarketInstruction::CancelOrderV2(cancel.clone());
instruction.data = cancel_instruction.pack();
solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds)?;
}
Ok(())
}
fn invoke_transfer<'a>(
token_prog_acc: &AccountInfo<'a>,
source_acc: &AccountInfo<'a>,
dest_acc: &AccountInfo<'a>,
authority_acc: &AccountInfo<'a>,
signers_seeds: &[&[&[u8]]],
quantity: u64
) -> ProgramResult {
let transfer_instruction = spl_token::instruction::transfer(
&spl_token::ID,
source_acc.key,
dest_acc.key,
authority_acc.key,
&[],
quantity
)?;
let accs = [
token_prog_acc.clone(),
source_acc.clone(),
dest_acc.clone(),
authority_acc.clone()
];
solana_program::program::invoke_signed(&transfer_instruction, &accs, signers_seeds)
}
fn get_in_out_quantities(
mango_group: &mut MangoGroup,
margin_account: &mut MarginAccount,
open_orders_accs: &[AccountInfo; NUM_MARKETS],
prices: &[U64F64; NUM_TOKENS],
in_token_index: usize,
out_token_index: usize,
liqor_max_in: u64
) -> MangoResult<(u64, u64)> {
let deficit_val = margin_account.get_partial_liq_deficit(&mango_group, &prices, open_orders_accs)? + ONE_U64F64;
let out_avail: U64F64 = margin_account.deposits[out_token_index].checked_mul(mango_group.indexes[out_token_index].deposit).unwrap();
let out_avail_val = out_avail * prices[out_token_index];
// liq incentive is max of 1/2 the dist between
// Can only deposit as much as it is possible to withdraw out_token
let max_in_val = out_avail_val / PARTIAL_LIQ_INCENTIVE;
let max_in_val = min(deficit_val, max_in_val);
// we know prices are not 0; if they are this will error;
let max_in: U64F64 = max_in_val / prices[in_token_index];
let native_borrow = margin_account.borrows[in_token_index].checked_mul(
mango_group.indexes[in_token_index].borrow).unwrap();
// Can only deposit as much there is borrows to offset in in_token
let in_quantity = min(min(max_in, native_borrow), U64F64::from_num(liqor_max_in));
let deposit: U64F64 = in_quantity / mango_group.indexes[in_token_index].borrow;
// TODO if borrowed is close to Deposit, just set borrowed == 0
checked_sub_borrow(mango_group, margin_account, in_token_index, deposit)?;
// Withdraw incentive funds to liqor
let in_val: U64F64 = in_quantity.checked_mul(prices[in_token_index]).unwrap();
let out_val: U64F64 = in_val * PARTIAL_LIQ_INCENTIVE;
let out_quantity: U64F64 = out_val / prices[out_token_index];
let withdraw = out_quantity / mango_group.indexes[out_token_index].deposit;
checked_sub_deposit(mango_group, margin_account, out_token_index, withdraw)?;
// TODO account for the rounded amounts as deposits -- could be valuable in some tokens
Ok((in_quantity.checked_ceil().unwrap().to_num(), out_quantity.checked_floor().unwrap().to_num()))
}