Merge branch 'dev'

This commit is contained in:
microwavedcola1 2022-07-14 08:52:12 +02:00
commit efed3a03ab
98 changed files with 11733 additions and 7608 deletions

View File

@ -9,7 +9,7 @@ on:
env:
CARGO_TERM_COLOR: always
SOLANA_VERSION: '1.9.14'
SOLANA_VERSION: '1.10.29'
RUST_TOOLCHAIN: 1.60.0
LOG_PROGRAM: 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD'
@ -65,10 +65,12 @@ jobs:
echo "Generating keypair..."
solana-keygen new -o "$HOME/.config/solana/id.json" --no-passphrase --silent
- name: Build all deps
run: cargo build-bpf
run: |
cargo build-bpf || true
cargo +bpf build-bpf
# Run bpf tests and output to runner and log
- name: Run tests
run: cargo test-bpf 2> >(tee raw-test-bpf.log >&2)
run: cargo +bpf test-bpf 2> >(tee raw-test-bpf.log >&2)
- name: Save raw log
uses: actions/upload-artifact@v3
with:

View File

@ -10,7 +10,7 @@ on:
env:
CARGO_TERM_COLOR: always
SOLANA_VERSION: "1.9.14"
SOLANA_VERSION: "1.10.29"
jobs:
build:

26
CHANGELOG.md Normal file
View File

@ -0,0 +1,26 @@
# Mango v4 Program Change Log
Update this for each mainnet deployment.
## not on mainnet
- Account data was rearranged to put fields that are often used with gPA first
- The `CreateGroup` instruction now requires an `insurance_mint` account, which is
used as the mint for the `insurance_vault` token account it creates. Pass the
USDC mint address.
- The token with `token_index` zero is now required to be the `insurance_mint`.
Trying to register a different token for index zero will now fail.
- New instruction: `LiqTokenBankruptcy` to resolve insurance fund payouts and
socialized loss for bankrupt accounts.
- The `PerpCreateMarket` instruction no longer requires a `quote_token_index`
argument. The USDC/insurance mint is always used as quote currency for perps.
- The `UpdateIndex` instruction now requires the `oracle` account to be passed
for logging purposes.
- New instructions: `AccountEdit`, `TokenEdit`, `PerpEditMarket` for reconfiguring.
- The `delegate` field on `MangoAccount` is now used and many instructions can be
called by the account delegate.
- Renamed instructions:
- create/close_group -> group_create/close
- create/edit/close_account -> account_create/edit/close
- update_index -> token_update_index
- create/set_stub_oracle -> stub_oracle_create/set

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,
}),
@ -103,7 +103,7 @@ impl MangoClient {
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::CreateAccount {
&mango_v4::accounts::AccountCreate {
group,
owner: payer.pubkey(),
account: {
@ -124,7 +124,7 @@ impl MangoClient {
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::CreateAccount {
&mango_v4::instruction::AccountCreate {
account_num,
name: mango_account_name.to_owned(),
},
@ -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

@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
use crate::MangoClient;
use anchor_lang::__private::bytemuck::cast_ref;
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
use futures::Future;
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex};
use solana_sdk::{
@ -19,7 +19,7 @@ pub async fn runner(
.banks_cache
.values()
.map(|banks_for_a_token| {
loop_update_index(
loop_update_index_and_rate(
mango_client.clone(),
banks_for_a_token.get(0).unwrap().1.token_index,
)
@ -48,7 +48,7 @@ pub async fn runner(
Ok(())
}
pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: TokenIndex) {
pub async fn loop_update_index_and_rate(mango_client: Arc<MangoClient>, token_index: TokenIndex) {
let mut interval = time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
@ -58,7 +58,9 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let mint_info = client.get_mint_info(&token_index);
let banks_for_a_token = client.banks_cache_by_token_index.get(&token_index).unwrap();
let token_name = banks_for_a_token.get(0).unwrap().1.name();
let some_bank = banks_for_a_token.get(0).unwrap().1;
let token_name = some_bank.name();
let oracle = some_bank.oracle;
let bank_pubkeys_for_a_token = banks_for_a_token
.into_iter()
@ -72,11 +74,15 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
let mut ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::UpdateIndex { mint_info },
&mango_v4::accounts::TokenUpdateIndexAndRate {
mint_info,
oracle,
instructions: solana_program::sysvar::instructions::id(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::UpdateIndex {},
&mango_v4::instruction::TokenUpdateIndexAndRate {},
),
};
let mut banks = bank_pubkeys_for_a_token
@ -95,7 +101,11 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
if let Err(e) = sig_result {
log::error!("{:?}", e)
} else {
log::info!("update_index {} {:?}", token_name, sig_result.unwrap())
log::info!(
"update_index_and_rate {} {:?}",
token_name,
sig_result.unwrap()
)
}
Ok(())

View File

@ -9,14 +9,16 @@
"url": "https://blockworks.foundation"
},
"main": "dist/cjs/src/index.js",
"module": "dist/esm/src/index.js",
"types": "dist/types/src/index.d.ts",
"sideEffects": false,
"files": [
"dist"
],
"scripts": {
"build": "npm run build:cjs",
"build": "npm run build:esm; npm run build:cjs",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"clean": "rm -rf dist",
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",
@ -30,6 +32,7 @@
"validate": "npm run typecheck && npm run test && npm run lint && npm run format-check"
},
"devDependencies": {
"@jup-ag/core": "^1.0.0-beta.28",
"@tsconfig/recommended": "^1.0.1",
"@types/bs58": "^4.0.1",
"@types/chai": "^4.3.0",
@ -55,7 +58,6 @@
"trailingComma": "all"
},
"dependencies": {
"@jup-ag/core": "^1.0.0-beta.27",
"@project-serum/anchor": "^0.24.2",
"@project-serum/serum": "^0.13.65",
"@pythnetwork/client": "^2.7.0",

View File

@ -1,4 +1,5 @@
use anchor_lang::prelude::*;
use core::fmt::Display;
// todo: group error blocks by kind
// todo: add comments which indicate decimal code for an error
@ -6,16 +7,111 @@ use anchor_lang::prelude::*;
pub enum MangoError {
#[msg("")]
SomeError,
#[msg("")]
#[msg("checked math error")]
MathError,
#[msg("")]
UnexpectedOracle,
#[msg("")]
#[msg("oracle type cannot be determined")]
UnknownOracleType,
#[msg("")]
InvalidFlashLoanTargetCpiProgram,
#[msg("")]
#[msg("health must be positive")]
HealthMustBePositive,
#[msg("The account is bankrupt")]
#[msg("the account is bankrupt")]
IsBankrupt,
#[msg("the account is not bankrupt")]
IsNotBankrupt,
#[msg("no free token position index")]
NoFreeTokenPositionIndex,
#[msg("no free serum3 open orders index")]
NoFreeSerum3OpenOrdersIndex,
#[msg("no free perp position index")]
NoFreePerpPositionIndex,
#[msg("serum3 open orders exist already")]
Serum3OpenOrdersExistAlready,
}
pub trait Contextable {
/// Add a context string `c` to a Result or Error
///
/// Example: foo().context("calling foo")?;
fn context(self, c: impl Display) -> Self;
/// Like `context()`, but evaluate the context string lazily
///
/// Use this if it's expensive to generate, like a format!() call.
fn with_context<C, F>(self, c: F) -> Self
where
C: Display,
F: FnOnce() -> C;
}
impl Contextable for Error {
fn context(self, c: impl Display) -> Self {
match self {
Error::AnchorError(err) => Error::AnchorError(AnchorError {
error_msg: if err.error_msg.is_empty() {
format!("{}", c)
} else {
format!("{}; {}", err.error_msg, c)
},
..err
}),
// Maybe wrap somehow?
Error::ProgramError(err) => Error::ProgramError(err),
}
}
fn with_context<C, F>(self, c: F) -> Self
where
C: Display,
F: FnOnce() -> C,
{
self.context(c())
}
}
impl<T> Contextable for Result<T> {
fn context(self, c: impl Display) -> Self {
if let Err(err) = self {
Err(err.context(c))
} else {
self
}
}
fn with_context<C, F>(self, c: F) -> Self
where
C: Display,
F: FnOnce() -> C,
{
if let Err(err) = self {
Err(err.context(c()))
} else {
self
}
}
}
/// Creates an Error with a particular message, using format!() style arguments
///
/// Example: error_msg!("index {} not found", index)
#[macro_export]
macro_rules! error_msg {
($($arg:tt)*) => {
error!(MangoError::SomeError).context(format!($($arg)*))
};
}
/// Like anchor's require!(), but with a customizable message
///
/// Example: require!(condition, "the condition on account {} was violated", account_key);
#[macro_export]
macro_rules! require_msg {
($invariant:expr, $($arg:tt)*) => {
if !($invariant) {
Err(error_msg!($($arg)*))?;
}
};
}
pub use error_msg;
pub use require_msg;

View File

@ -1,11 +1,11 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::state::{PerpMarketIndex, TokenIndex};
use crate::state::{HealthCache, PerpMarketIndex, TokenIndex};
#[event]
#[derive(Debug)]
pub struct MangoAccountData {
pub health_cache: HealthCache,
pub init_health: I80F48,
pub maint_health: I80F48,
pub equity: Equity,

View File

@ -1,14 +1,16 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Token;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
pub struct CloseAccount<'info> {
pub struct AccountClose<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
// note: should never be the delegate
has_one = owner,
has_one = group,
close = sol_destination
@ -23,7 +25,7 @@ pub struct CloseAccount<'info> {
pub token_program: Program<'info, Token>,
}
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
let group = ctx.accounts.group.load()?;
// don't perform checks if group is just testing
@ -32,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

@ -6,7 +6,7 @@ use crate::util::fill32_from_str;
#[derive(Accounts)]
#[instruction(account_num: u8)]
pub struct CreateAccount<'info> {
pub struct AccountCreate<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
@ -17,7 +17,6 @@ pub struct CreateAccount<'info> {
space = 8 + std::mem::size_of::<MangoAccount>(),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(mut)]
@ -26,7 +25,7 @@ pub struct CreateAccount<'info> {
pub system_program: Program<'info, System>,
}
pub fn create_account(ctx: Context<CreateAccount>, account_num: u8, name: String) -> Result<()> {
pub fn account_create(ctx: Context<AccountCreate>, account_num: u8, name: String) -> Result<()> {
let mut account = ctx.accounts.account.load_init()?;
account.name = fill32_from_str(name)?;
@ -38,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

@ -0,0 +1,62 @@
use crate::error::MangoError;
use anchor_lang::prelude::*;
use crate::state::*;
use crate::util::fill32_from_str;
#[derive(Accounts)]
pub struct AccountEdit<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
// Note: should never be the delegate
has_one = owner,
has_one = group,
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
}
pub fn account_edit(
ctx: Context<AccountEdit>,
name_opt: Option<String>,
// note: can also be used to unset by using the default pubkey here as a param
delegate_opt: Option<Pubkey>,
) -> Result<()> {
require!(
name_opt.is_some() || delegate_opt.is_some(),
MangoError::SomeError
);
let mut account = ctx.accounts.account.load_mut()?;
// msg!("old account {:#?}", account);
// note: unchanged fields are inline, and match exact definition in create_account
// please maintain, and don't remove, makes it easy to reason about which support modification by owner
if let Some(name) = name_opt {
account.name = fill32_from_str(name)?;
}
// unchanged -
// owner
// account_num
// bump
if let Some(delegate) = delegate_opt {
account.delegate = delegate;
}
// unchanged -
// tokens
// serum3
// perps
// being_liquidated
// is_bankrupt
// msg!("new account {:#?}", account);
Ok(())
}

View File

@ -17,15 +17,17 @@ pub fn compute_account_data(ctx: Context<ComputeAccountData>) -> Result<()> {
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?;
let init_health = compute_health(&account, HealthType::Init, &account_retriever)?;
let maint_health = compute_health(&account, HealthType::Maint, &account_retriever)?;
let health_cache = new_health_cache(&account, &account_retriever)?;
let init_health = health_cache.health(HealthType::Init);
let maint_health = health_cache.health(HealthType::Maint);
let equity = compute_equity(&account, &account_retriever)?;
emit!(MangoAccountData {
health_cache,
init_health,
maint_health,
equity
equity,
});
Ok(())

View File

@ -32,7 +32,7 @@ pub struct FlashLoan<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
@ -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

@ -37,11 +37,12 @@ pub struct FlashLoan2End<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = owner,
has_one = group,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
pub token_program: Program<'info, Token>,
}
@ -161,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

@ -1,5 +1,5 @@
use crate::accounts_zerocopy::*;
use crate::error::MangoError;
use crate::error::*;
use crate::group_seeds;
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
use crate::state::{
@ -40,10 +40,11 @@ pub struct FlashLoan3Begin<'info> {
pub struct FlashLoan3End<'info> {
#[account(
mut,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
pub token_program: Program<'info, Token>,
}
@ -54,11 +55,7 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
loan_amounts: Vec<u64>,
) -> Result<()> {
let num_loans = loan_amounts.len();
require_eq!(
ctx.remaining_accounts.len(),
3 * num_loans,
MangoError::SomeError
);
require_eq!(ctx.remaining_accounts.len(), 3 * num_loans);
let banks = &ctx.remaining_accounts[..num_loans];
let vaults = &ctx.remaining_accounts[num_loans..2 * num_loans];
let token_accounts = &ctx.remaining_accounts[2 * num_loans..];
@ -105,10 +102,9 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
// Forbid FlashLoan3Begin to be called from CPI (it does not have to be the first instruction)
let current_ix = tx_instructions::load_instruction_at_checked(current_index, ixs)?;
require_keys_eq!(
current_ix.program_id,
*ctx.program_id,
MangoError::SomeError
require_msg!(
current_ix.program_id == *ctx.program_id,
"FlashLoan3Begin must be a top-level instruction"
);
// The only other mango instruction that must appear before the end of the tx is
@ -119,19 +115,22 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
Ok(ix) => ix,
Err(ProgramError::InvalidArgument) => break, // past the last instruction
Err(e) => Err(e)?,
Err(e) => return Err(e.into()),
};
// Check that the mango program key is not used
if ix.program_id == crate::id() {
// must be the last mango ix -- this could possibly be relaxed, but right now
// we need to guard against multiple FlashLoanEnds
require!(!found_end, MangoError::SomeError);
require_msg!(
!found_end,
"the transaction must not contain a Mango instruction after FlashLoan3End"
);
found_end = true;
// must be the FlashLoan3End instruction
require!(
&ix.data[0..8] == &[163, 231, 155, 56, 201, 68, 84, 148],
ix.data[0..8] == [163, 231, 155, 56, 201, 68, 84, 148],
MangoError::SomeError
);
@ -139,18 +138,21 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
let begin_accounts = &ctx.remaining_accounts[num_loans..];
let end_accounts = &ix.accounts[ix.accounts.len() - 2 * num_loans..];
for (begin_account, end_account) in begin_accounts.iter().zip(end_accounts.iter()) {
require_keys_eq!(*begin_account.key, end_account.pubkey);
require_msg!(*begin_account.key == end_account.pubkey, "the trailing accounts passed to FlashLoan3Begin and End must match, found {} on begin and {} on end", begin_account.key, end_account.pubkey);
}
} else {
// ensure no one can cpi into mango either
for meta in ix.accounts.iter() {
require_keys_neq!(meta.pubkey, crate::id());
require_msg!(meta.pubkey != crate::id(), "instructions between FlashLoan3Begin and End may not use the Mango program account");
}
}
index += 1;
}
require!(found_end, MangoError::SomeError);
require_msg!(
found_end,
"found no FlashLoan3End instruction in transaction"
);
}
Ok(())
@ -167,7 +169,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
@ -181,7 +183,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
maybe_token_account.unwrap().owner == account.group
})
.ok_or_else(|| error!(MangoError::SomeError))?;
.ok_or_else(|| error_msg!("expected at least one vault token account to be passed"))?;
let vaults_len = (ctx.remaining_accounts.len() - vaults_index) / 2;
require_eq!(ctx.remaining_accounts.len(), vaults_index + 2 * vaults_len);
@ -252,7 +254,14 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
}
// all vaults must have had matching banks
require!(vaults_with_banks.iter().all(|&b| b), MangoError::SomeError);
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
require_msg!(
has_bank,
"missing bank for vault index {}, address {}",
i,
vaults[i].key
);
}
// Check pre-cpi health
// NOTE: This health check isn't strictly necessary. It will be, later, when

View File

@ -3,7 +3,7 @@ use anchor_lang::prelude::*;
use anchor_spl::token::Token;
#[derive(Accounts)]
pub struct CloseGroup<'info> {
pub struct GroupClose<'info> {
#[account(
mut,
constraint = group.load()?.testing == 1,
@ -21,7 +21,7 @@ pub struct CloseGroup<'info> {
pub token_program: Program<'info, Token>,
}
pub fn close_group(_ctx: Context<CloseGroup>) -> Result<()> {
pub fn group_close(_ctx: Context<GroupClose>) -> Result<()> {
// TODO: checks
Ok(())
}

View File

@ -1,11 +1,12 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Mint, Token, TokenAccount};
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
#[instruction(group_num: u32)]
pub struct CreateGroup<'info> {
pub struct GroupCreate<'info> {
#[account(
init,
seeds = [b"Group".as_ref(), admin.key().as_ref(), &group_num.to_le_bytes()],
@ -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<()> {
pub fn group_create(ctx: Context<GroupCreate>, 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

@ -0,0 +1,217 @@
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::IsNotBankrupt);
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(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,9 +15,9 @@ pub struct LiqTokenWithToken<'info> {
#[account(
mut,
has_one = group,
constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()),
)]
pub liqor: AccountLoader<'info, MangoAccount>,
#[account(address = liqor.load()?.owner)]
pub liqor_owner: Signer<'info>,
#[account(
@ -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 {
let init_health = liqee_health_cache.health(HealthType::Init);
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)?;
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,16 +68,18 @@ 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
.get(asset_bank.token_index)?
.native(&asset_bank);
// The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position
// borrows alive at the same time. Possibly adding get_mut_pair() would be helpful.
let (liqee_asset_position, liqee_asset_raw_index) = liqee.tokens.get(asset_token_index)?;
let liqee_assets_native = liqee_asset_position.native(&asset_bank);
require!(liqee_assets_native.is_positive(), MangoError::SomeError);
let liqee_liab_native = liqee.tokens.get(liab_bank.token_index)?.native(&liab_bank);
let (liqee_liab_position, liqee_liab_raw_index) = liqee.tokens.get(liab_token_index)?;
let liqee_liab_native = liqee_liab_position.native(&liab_bank);
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
// TODO why sum of both tokens liquidation fees? Add comment
@ -115,18 +117,24 @@ pub fn liq_token_with_token(
let asset_transfer = cm!(liab_transfer * liab_price_adjusted / asset_price);
// Apply the balance changes to the liqor and liqee accounts
liab_bank.deposit(liqee.tokens.get_mut(liab_token_index)?, liab_transfer)?;
liab_bank.withdraw_with_fee(
liqor.tokens.get_mut_or_create(liab_token_index)?.0,
liab_transfer,
)?;
let liqee_liab_position = liqee.tokens.get_mut_raw(liqee_liab_raw_index);
let liqee_liab_active = liab_bank.deposit(liqee_liab_position, liab_transfer)?;
let liqee_liab_position_indexed = liqee_liab_position.indexed_position;
asset_bank.deposit(
liqor.tokens.get_mut_or_create(asset_token_index)?.0,
asset_transfer,
)?;
asset_bank
.withdraw_without_fee(liqee.tokens.get_mut(asset_token_index)?, asset_transfer)?;
let (liqor_liab_position, liqor_liab_raw_index, _) =
liqor.tokens.get_mut_or_create(liab_token_index)?;
let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.tokens.get_mut_or_create(asset_token_index)?;
let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?;
let liqor_asset_position_indexed = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.tokens.get_mut_raw(liqee_asset_raw_index);
let liqee_asset_active =
asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?;
let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
// Update the health cache
liqee_health_cache.adjust_token_balance(liab_token_index, liab_transfer)?;
@ -154,11 +162,7 @@ pub fn liq_token_with_token(
emit!(TokenBalanceLog {
mango_account: ctx.accounts.liqee.key(),
token_index: asset_token_index,
indexed_position: liqee
.tokens
.get_mut(asset_token_index)?
.indexed_position
.to_bits(),
indexed_position: liqee_asset_position_indexed.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
price: asset_price.to_bits(),
@ -167,11 +171,7 @@ pub fn liq_token_with_token(
emit!(TokenBalanceLog {
mango_account: ctx.accounts.liqee.key(),
token_index: liab_token_index,
indexed_position: liqee
.tokens
.get_mut(liab_token_index)?
.indexed_position
.to_bits(),
indexed_position: liqee_liab_position_indexed.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
price: liab_price.to_bits(),
@ -180,11 +180,7 @@ pub fn liq_token_with_token(
emit!(TokenBalanceLog {
mango_account: ctx.accounts.liqor.key(),
token_index: asset_token_index,
indexed_position: liqor
.tokens
.get_mut(asset_token_index)?
.indexed_position
.to_bits(),
indexed_position: liqor_asset_position_indexed.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
price: asset_price.to_bits(),
@ -193,34 +189,42 @@ pub fn liq_token_with_token(
emit!(TokenBalanceLog {
mango_account: ctx.accounts.liqor.key(),
token_index: liab_token_index,
indexed_position: liqor
.tokens
.get_mut(liab_token_index)?
.indexed_position
.to_bits(),
indexed_position: liqor_liab_position_indexed.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
price: liab_price.to_bits(),
});
// Since we use a scanning account retriever, it's safe to deactivate inactive token positions
if !liqee_asset_active {
liqee.tokens.deactivate(liqee_asset_raw_index);
}
if !liqee_liab_active {
liqee.tokens.deactivate(liqee_liab_raw_index);
}
if !liqor_asset_active {
liqor.tokens.deactivate(liqor_asset_raw_index);
}
if !liqor_liab_active {
liqor.tokens.deactivate(liqor_liab_raw_index)
}
}
// Check liqee health again
let maint_health = liqee_health_cache.health(HealthType::Maint)?;
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)?;
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
let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever)?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
// TOOD: this must deactivate token accounts if the deposit/withdraw calls above call for it
Ok(())
}

View File

@ -1,14 +1,14 @@
pub use account_close::*;
pub use account_create::*;
pub use account_edit::*;
pub use benchmark::*;
pub use close_account::*;
pub use close_group::*;
pub use close_stub_oracle::*;
pub use compute_account_data::*;
pub use create_account::*;
pub use create_group::*;
pub use create_stub_oracle::*;
pub use flash_loan::*;
pub use flash_loan2::*;
pub use flash_loan3::*;
pub use group_close::*;
pub use group_create::*;
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::*;
@ -17,6 +17,7 @@ pub use perp_cancel_order_by_client_order_id::*;
pub use perp_close_market::*;
pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_edit_market::*;
pub use perp_place_order::*;
pub use perp_update_funding::*;
pub use serum3_cancel_all_orders::*;
@ -28,25 +29,28 @@ pub use serum3_liq_force_cancel_orders::*;
pub use serum3_place_order::*;
pub use serum3_register_market::*;
pub use serum3_settle_funds::*;
pub use set_stub_oracle::*;
pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
pub use token_register::*;
pub use token_update_index_and_rate::*;
pub use token_withdraw::*;
pub use update_index::*;
mod account_close;
mod account_create;
mod account_edit;
mod benchmark;
mod close_account;
mod close_group;
mod close_stub_oracle;
mod compute_account_data;
mod create_account;
mod create_group;
mod create_stub_oracle;
mod flash_loan;
mod flash_loan2;
mod flash_loan3;
mod group_close;
mod group_create;
mod liq_token_bankruptcy;
mod liq_token_with_token;
mod perp_cancel_all_orders;
mod perp_cancel_all_orders_by_side;
@ -55,6 +59,7 @@ mod perp_cancel_order_by_client_order_id;
mod perp_close_market;
mod perp_consume_events;
mod perp_create_market;
mod perp_edit_market;
mod perp_place_order;
mod perp_update_funding;
mod serum3_cancel_all_orders;
@ -66,10 +71,13 @@ mod serum3_liq_force_cancel_orders;
mod serum3_place_order;
mod serum3_register_market;
mod serum3_settle_funds;
mod set_stub_oracle;
mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_deposit;
mod token_deregister;
mod token_edit;
mod token_register;
mod token_update_index_and_rate;
mod token_withdraw;
mod update_index;

View File

@ -10,9 +10,10 @@ pub struct PerpCancelAllOrders<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
@ -25,13 +26,11 @@ pub struct PerpCancelAllOrders<'info> {
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub owner: Signer<'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

@ -10,9 +10,10 @@ pub struct PerpCancelAllOrdersBySide<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
@ -25,8 +26,6 @@ pub struct PerpCancelAllOrdersBySide<'info> {
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub owner: Signer<'info>,
}
pub fn perp_cancel_all_orders_by_side(
@ -35,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

@ -10,9 +10,10 @@ pub struct PerpCancelOrder<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
@ -25,13 +26,11 @@ pub struct PerpCancelOrder<'info> {
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub owner: Signer<'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

@ -10,9 +10,10 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
@ -25,8 +26,6 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
pub bids: AccountLoader<'info, BookSide>,
pub owner: Signer<'info>,
}
pub fn perp_cancel_order_by_client_order_id(
@ -34,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

@ -0,0 +1,118 @@
use crate::state::*;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
#[derive(Accounts)]
pub struct PerpEditMarket<'info> {
#[account(
has_one = admin,
)]
pub group: AccountLoader<'info, Group>,
pub admin: Signer<'info>,
#[account(
mut,
has_one = group
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
}
#[allow(clippy::too_many_arguments)]
pub fn perp_edit_market(
ctx: Context<PerpEditMarket>,
oracle_opt: Option<Pubkey>,
oracle_config_opt: Option<OracleConfig>,
base_token_index_opt: Option<TokenIndex>,
base_token_decimals_opt: Option<u8>,
maint_asset_weight_opt: Option<f32>,
init_asset_weight_opt: Option<f32>,
maint_liab_weight_opt: Option<f32>,
init_liab_weight_opt: Option<f32>,
liquidation_fee_opt: Option<f32>,
maker_fee_opt: Option<f32>,
taker_fee_opt: Option<f32>,
min_funding_opt: Option<f32>,
max_funding_opt: Option<f32>,
impact_quantity_opt: Option<i64>,
) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
// note: unchanged fields are inline, and match exact definition in perp_register_market
// please maintain, and don't remove, makes it easy to reason about which support admin modification
// unchanged -
// name
// group
if let Some(oracle) = oracle_opt {
perp_market.oracle = oracle;
}
if let Some(oracle_config) = oracle_config_opt {
perp_market.oracle_config = oracle_config;
};
// unchanged -
// bids
// asks
// event_queue
// quote_lot_size
// base_lot_size
if let Some(maint_asset_weight) = maint_asset_weight_opt {
perp_market.maint_asset_weight = I80F48::from_num(maint_asset_weight);
}
if let Some(init_asset_weight) = init_asset_weight_opt {
perp_market.init_asset_weight = I80F48::from_num(init_asset_weight);
}
if let Some(maint_liab_weight) = maint_liab_weight_opt {
perp_market.maint_liab_weight = I80F48::from_num(maint_liab_weight);
}
if let Some(init_liab_weight) = init_liab_weight_opt {
perp_market.init_liab_weight = I80F48::from_num(init_liab_weight);
}
if let Some(liquidation_fee) = liquidation_fee_opt {
perp_market.liquidation_fee = I80F48::from_num(liquidation_fee);
}
if let Some(maker_fee) = maker_fee_opt {
perp_market.maker_fee = I80F48::from_num(maker_fee);
}
if let Some(taker_fee) = taker_fee_opt {
perp_market.taker_fee = I80F48::from_num(taker_fee);
}
if let Some(min_funding) = min_funding_opt {
perp_market.min_funding = I80F48::from_num(min_funding);
}
if let Some(max_funding) = max_funding_opt {
perp_market.max_funding = I80F48::from_num(max_funding);
}
if let Some(impact_quantity) = impact_quantity_opt {
perp_market.impact_quantity = impact_quantity;
}
// unchanged -
// long_funding
// short_funding
// funding_last_updated
// open_interest
// seq_num
// fees_accrued
// bump
if let Some(base_token_decimals) = base_token_decimals_opt {
perp_market.base_token_decimals = base_token_decimals;
}
// unchanged -
// perp_market_index
if let Some(base_token_index) = base_token_index_opt {
perp_market.base_token_index = base_token_index;
}
// unchanged -
// quote_token_index
Ok(())
}

View File

@ -14,9 +14,10 @@ pub struct PerpPlaceOrder<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
@ -36,8 +37,6 @@ pub struct PerpPlaceOrder<'info> {
/// CHECK: The oracle can be one of several different account types and the pubkey is checked above
pub oracle: UncheckedAccount<'info>,
pub owner: Signer<'info>,
}
// TODO
@ -80,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

@ -10,7 +10,7 @@ pub struct Serum3CancelAllOrders<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -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

@ -17,7 +17,7 @@ pub struct Serum3CancelOrder<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -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

@ -10,7 +10,7 @@ pub struct Serum3CloseOpenOrders<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -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

@ -10,9 +10,10 @@ pub struct Serum3CreateOpenOrders<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
has_one = group,
@ -38,8 +39,6 @@ pub struct Serum3CreateOpenOrders<'info> {
/// CHECK: Newly created by serum cpi call
pub open_orders: UncheckedAccount<'info>,
pub owner: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
@ -52,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

@ -1,8 +1,8 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use fixed::types::I80F48;
use crate::error::*;
use crate::instructions::apply_vault_difference;
use crate::state::*;
#[derive(Accounts)]
@ -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
@ -139,23 +139,19 @@ pub fn serum3_liq_force_cancel_orders(
let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account
{
let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let base_position = account.tokens.get_mut(base_bank.token_index)?;
base_bank.deposit(
base_position,
I80F48::from(after_base_vault) - I80F48::from(before_base_vault),
)?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let quote_position = account.tokens.get_mut(quote_bank.token_index)?;
quote_bank.deposit(
quote_position,
I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault),
)?;
}
let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
apply_vault_difference(
&mut account,
&mut base_bank,
after_base_vault,
before_base_vault,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?
.deactivate_inactive_token_accounts(&mut account);
Ok(())
}

View File

@ -86,7 +86,7 @@ pub struct Serum3PlaceOrder<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -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!(
@ -260,25 +260,31 @@ pub fn serum3_place_order(
let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account
apply_vault_difference(
ctx.accounts.account.load_mut()?,
ctx.accounts.base_bank.load_mut()?,
after_base_vault,
before_base_vault,
ctx.accounts.quote_bank.load_mut()?,
after_quote_vault,
before_quote_vault,
)?;
let mut account = ctx.accounts.account.load_mut()?;
let vault_difference_result = {
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
apply_vault_difference(
&mut account,
&mut base_bank,
after_base_vault,
before_base_vault,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?
};
//
// Health check
//
let account = ctx.accounts.account.load()?;
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?;
let health = compute_health(&account, HealthType::Init, &retriever)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);
vault_difference_result.deactivate_inactive_token_accounts(&mut account);
Ok(())
}
@ -305,28 +311,51 @@ pub fn inc_maybe_loan(
}
}
pub struct VaultDifferenceResult {
base_raw_index: usize,
base_active: bool,
quote_raw_index: usize,
quote_active: bool,
}
impl VaultDifferenceResult {
pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccount) {
if !self.base_active {
account.tokens.deactivate(self.base_raw_index);
}
if !self.quote_active {
account.tokens.deactivate(self.quote_raw_index);
}
}
}
pub fn apply_vault_difference(
mut account: std::cell::RefMut<MangoAccount>,
mut base_bank: std::cell::RefMut<Bank>,
account: &mut MangoAccount,
base_bank: &mut Bank,
after_base_vault: u64,
before_base_vault: u64,
mut quote_bank: std::cell::RefMut<Bank>,
quote_bank: &mut Bank,
after_quote_vault: u64,
before_quote_vault: u64,
) -> Result<()> {
) -> Result<VaultDifferenceResult> {
// TODO: Applying the loan origination fee here may be too early: it should only be
// charged if an order executes and the loan materializes? Otherwise MMs that place
// an order without having the funds will be charged for each place_order!
let base_position = account.tokens.get_mut(base_bank.token_index)?;
let (base_position, base_raw_index) = account.tokens.get_mut(base_bank.token_index)?;
let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault);
base_bank.change_with_fee(base_position, base_change)?;
let base_active = base_bank.change_with_fee(base_position, base_change)?;
let quote_position = account.tokens.get_mut(quote_bank.token_index)?;
let (quote_position, quote_raw_index) = account.tokens.get_mut(quote_bank.token_index)?;
let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault);
quote_bank.change_with_fee(quote_position, quote_change)?;
let quote_active = quote_bank.change_with_fee(quote_position, quote_change)?;
Ok(())
Ok(VaultDifferenceResult {
base_raw_index,
base_active,
quote_raw_index,
quote_active,
})
}
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {

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

@ -18,7 +18,7 @@ pub struct Serum3SettleFunds<'info> {
#[account(
mut,
has_one = group,
has_one = owner,
constraint = account.load()?.is_owner_or_delegate(owner.key()),
)]
pub account: AccountLoader<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -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!(
@ -149,17 +149,19 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account
let base_bank = ctx.accounts.base_bank.load_mut()?;
let quote_bank = ctx.accounts.quote_bank.load_mut()?;
let mut account = ctx.accounts.account.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
apply_vault_difference(
ctx.accounts.account.load_mut()?,
base_bank,
&mut account,
&mut base_bank,
after_base_vault,
before_base_vault,
quote_bank,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?;
)?
.deactivate_inactive_token_accounts(&mut account);
}
Ok(())
@ -185,7 +187,7 @@ pub fn charge_maybe_fees(
serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved();
// loan origination fees
let coin_token_account = account.tokens.get_mut(coin_bank.token_index)?;
let coin_token_account = account.tokens.get_mut(coin_bank.token_index)?.0;
let coin_token_native = coin_token_account.native(&coin_bank);
if coin_token_native.is_negative() {
@ -195,7 +197,7 @@ pub fn charge_maybe_fees(
// charge the loan origination fee
coin_bank
.borrow_mut()
.charge_loan_origination_fee(coin_token_account, actualized_loan)?;
.withdraw_loan_origination_fee(coin_token_account, actualized_loan)?;
}
}
@ -209,7 +211,7 @@ pub fn charge_maybe_fees(
serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved();
// loan origination fees
let pc_token_account = account.tokens.get_mut(pc_bank.token_index)?;
let pc_token_account = account.tokens.get_mut(pc_bank.token_index)?.0;
let pc_token_native = pc_token_account.native(&pc_bank);
if pc_token_native.is_negative() {
@ -219,7 +221,7 @@ pub fn charge_maybe_fees(
// charge the loan origination fee
pc_bank
.borrow_mut()
.charge_loan_origination_fee(pc_token_account, actualized_loan)?;
.withdraw_loan_origination_fee(pc_token_account, actualized_loan)?;
}
}

View File

@ -4,7 +4,7 @@ use anchor_spl::token::Token;
use crate::state::*;
#[derive(Accounts)]
pub struct CloseStubOracle<'info> {
pub struct StubOracleClose<'info> {
#[account(
constraint = group.load()?.testing == 1,
has_one = admin,
@ -27,6 +27,6 @@ pub struct CloseStubOracle<'info> {
pub token_program: Program<'info, Token>,
}
pub fn close_stub_oracle(_ctx: Context<CloseStubOracle>) -> Result<()> {
pub fn stub_oracle_close(_ctx: Context<StubOracleClose>) -> Result<()> {
Ok(())
}

View File

@ -5,7 +5,7 @@ use fixed::types::I80F48;
use crate::state::*;
#[derive(Accounts)]
pub struct CreateStubOracle<'info> {
pub struct StubOracleCreate<'info> {
#[account(
has_one = admin,
)]
@ -30,7 +30,7 @@ pub struct CreateStubOracle<'info> {
pub system_program: Program<'info, System>,
}
pub fn create_stub_oracle(ctx: Context<CreateStubOracle>, price: I80F48) -> Result<()> {
pub fn stub_oracle_create(ctx: Context<StubOracleCreate>, price: I80F48) -> Result<()> {
let mut oracle = ctx.accounts.oracle.load_init()?;
oracle.group = ctx.accounts.group.key();
oracle.mint = ctx.accounts.token_mint.key();

View File

@ -4,7 +4,7 @@ use fixed::types::I80F48;
use crate::state::*;
#[derive(Accounts)]
pub struct SetStubOracle<'info> {
pub struct StubOracleSet<'info> {
#[account(
has_one = admin,
)]
@ -22,8 +22,7 @@ pub struct SetStubOracle<'info> {
pub payer: Signer<'info>,
}
// TODO: add admin requirement for changing price
pub fn set_stub_oracle(ctx: Context<SetStubOracle>, price: I80F48) -> Result<()> {
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
let mut oracle = ctx.accounts.oracle.load_mut()?;
oracle.price = price;
oracle.last_updated = Clock::get()?.unix_timestamp;

View File

@ -6,6 +6,7 @@ use fixed::types::I80F48;
use crate::error::*;
use crate::state::*;
use crate::util::checked_math as cm;
use crate::logs::{DepositLog, TokenBalanceLog};
@ -54,20 +55,21 @@ impl<'info> TokenDeposit<'info> {
// That would save a lot of computation that needs to go into finding the
// right index for the mint.
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
require!(amount > 0, MangoError::SomeError);
require_msg!(amount > 0, "deposit amount must be positive");
let token_index = ctx.accounts.bank.load()?.token_index;
// 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)?;
let amount_i80f48 = I80F48::from(amount);
let position_is_active = {
let mut bank = ctx.accounts.bank.load_mut()?;
bank.deposit(position, I80F48::from(amount))?
bank.deposit(position, amount_i80f48)?
};
// Transfer the actual tokens
@ -78,6 +80,10 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?;
let (bank, oracle_price) =
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
account.net_deposits += cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::<f32>();
emit!(TokenBalanceLog {
mango_account: ctx.accounts.account.key(),
token_index: token_index,
@ -92,7 +98,8 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
// TODO: This will be used to disable is_bankrupt or being_liquidated
// when health recovers sufficiently
//
let health = compute_health(&account, HealthType::Init, &retriever)?;
let health = compute_health(&account, HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
//

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

@ -0,0 +1,122 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use super::InterestRateParams;
use crate::accounts_zerocopy::LoadMutZeroCopyRef;
use crate::state::*;
#[derive(Accounts)]
#[instruction(token_index: TokenIndex, bank_num: u64)]
pub struct TokenEdit<'info> {
#[account(
has_one = admin,
)]
pub group: AccountLoader<'info, Group>,
pub admin: Signer<'info>,
#[account(
has_one = group
)]
pub mint_info: AccountLoader<'info, MintInfo>,
}
#[allow(unused_variables)]
#[allow(clippy::too_many_arguments)]
pub fn token_edit(
ctx: Context<TokenEdit>,
bank_num: u64,
oracle_opt: Option<Pubkey>,
oracle_config_opt: Option<OracleConfig>,
interest_rate_params_opt: Option<InterestRateParams>,
loan_fee_rate_opt: Option<f32>,
loan_origination_fee_rate_opt: Option<f32>,
maint_asset_weight_opt: Option<f32>,
init_asset_weight_opt: Option<f32>,
maint_liab_weight_opt: Option<f32>,
init_liab_weight_opt: Option<f32>,
liquidation_fee_opt: Option<f32>,
) -> Result<()> {
ctx.accounts
.mint_info
.load()?
.verify_banks_ais(ctx.remaining_accounts)?;
for ai in ctx.remaining_accounts.iter() {
let mut bank = ai.load_mut::<Bank>()?;
// note: unchanged fields are inline, and match exact definition in register_token
// please maintain, and don't remove, makes it easy to reason about which support admin modification
// unchanged -
// name
// group
// mint
// vault
if let Some(oracle) = oracle_opt {
bank.oracle = oracle;
}
if let Some(oracle_config) = oracle_config_opt {
bank.oracle_config = oracle_config;
};
// unchanged -
// deposit_index
// borrow_index
// cached_indexed_total_deposits
// cached_indexed_total_borrows
// indexed_deposits
// indexed_borrows
// last_updated
if let Some(ref interest_rate_params) = interest_rate_params_opt {
// TODO: add a require! verifying relation between the parameters
bank.adjustment_factor = I80F48::from_num(interest_rate_params.adjustment_factor);
bank.util0 = I80F48::from_num(interest_rate_params.util0);
bank.rate0 = I80F48::from_num(interest_rate_params.rate0);
bank.util1 = I80F48::from_num(interest_rate_params.util1);
bank.rate1 = I80F48::from_num(interest_rate_params.rate1);
bank.max_rate = I80F48::from_num(interest_rate_params.max_rate);
}
// unchanged -
// collected_fees_native
if let Some(loan_origination_fee_rate) = loan_origination_fee_rate_opt {
bank.loan_origination_fee_rate = I80F48::from_num(loan_origination_fee_rate);
}
if let Some(loan_fee_rate) = loan_fee_rate_opt {
bank.loan_fee_rate = I80F48::from_num(loan_fee_rate);
}
if let Some(maint_asset_weight) = maint_asset_weight_opt {
bank.maint_asset_weight = I80F48::from_num(maint_asset_weight);
}
if let Some(init_asset_weight) = init_asset_weight_opt {
bank.init_asset_weight = I80F48::from_num(init_asset_weight);
}
if let Some(maint_liab_weight) = maint_liab_weight_opt {
bank.maint_liab_weight = I80F48::from_num(maint_liab_weight);
}
if let Some(init_liab_weight) = init_liab_weight_opt {
bank.init_liab_weight = I80F48::from_num(init_liab_weight);
}
if let Some(liquidation_fee) = liquidation_fee_opt {
bank.liquidation_fee = I80F48::from_num(liquidation_fee);
}
// unchanged -
// dust
// flash_loan_vault_initial
// flash_loan_approved_amount
// token_index
// bump
// mint_decimals
// bank_num
// reserved
}
Ok(())
}

View File

@ -82,6 +82,7 @@ pub struct InterestRateParams {
pub util1: f32,
pub rate1: f32,
pub max_rate: f32,
pub adjustment_factor: f32,
}
// TODO: should this be "configure_mint", we pass an explicit index, and allow
@ -106,6 +107,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)?,
@ -120,8 +129,11 @@ pub fn token_register(
cached_indexed_total_borrows: I80F48::ZERO,
indexed_deposits: I80F48::ZERO,
indexed_borrows: I80F48::ZERO,
last_updated: Clock::get()?.unix_timestamp,
index_last_updated: Clock::get()?.unix_timestamp,
bank_rate_last_updated: Clock::get()?.unix_timestamp,
// TODO: add a require! verifying relation between the parameters
avg_utilization: I80F48::ZERO,
adjustment_factor: I80F48::from_num(interest_rate_params.adjustment_factor),
util0: I80F48::from_num(interest_rate_params.util0),
rate0: I80F48::from_num(interest_rate_params.rate0),
util1: I80F48::from_num(interest_rate_params.util1),
@ -162,6 +174,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

@ -0,0 +1,163 @@
use anchor_lang::prelude::*;
use crate::error::MangoError;
use crate::logs::{UpdateIndexLog, UpdateRateLog};
use crate::state::HOUR;
use crate::{
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
state::{oracle_price, Bank, MintInfo},
};
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use checked_math as cm;
use fixed::types::I80F48;
#[derive(Accounts)]
pub struct TokenUpdateIndexAndRate<'info> {
#[account(
has_one = oracle
)]
pub mint_info: AccountLoader<'info, MintInfo>,
pub oracle: UncheckedAccount<'info>,
#[account(address = tx_instructions::ID)]
pub instructions: UncheckedAccount<'info>,
}
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
{
let ixs = ctx.accounts.instructions.as_ref();
let mut index = 0;
loop {
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
Ok(ix) => ix,
Err(ProgramError::InvalidArgument) => break,
Err(e) => return Err(e.into()),
};
// 1. we want to forbid token deposit and token withdraw and similar
// (serum3 place order could be used as a withdraw and a serum3 cancel order as a deposit)
// to be called in same tx as this ix to prevent index or rate manipulation,
// for now we just whitelist to other token_update_index_and_rate ix
// 2. we want to forbid cpi, since ix we would like to blacklist could just be called from cpi
require!(
ix.program_id == crate::id()
&& ix.data[0..8] == [131, 136, 194, 39, 11, 50, 10, 198], // token_update_index_and_rate
MangoError::SomeError
);
index += 1;
}
}
let mint_info = ctx.accounts.mint_info.load()?;
ctx.accounts
.mint_info
.load()?
.verify_banks_ais(ctx.remaining_accounts)?;
let now_ts = Clock::get()?.unix_timestamp;
// compute indexed_total
let mut indexed_total_deposits = I80F48::ZERO;
let mut indexed_total_borrows = I80F48::ZERO;
for ai in ctx.remaining_accounts.iter() {
let bank = ai.load::<Bank>()?;
indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits);
indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows);
}
// compute and set latest index and average utilization on each bank
{
let some_bank = ctx.remaining_accounts[0].load::<Bank>()?;
let now_ts_i80f48 = I80F48::from_num(now_ts);
let diff_ts = I80F48::from_num(now_ts - some_bank.index_last_updated);
let (deposit_index, borrow_index) =
some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?;
let new_avg_utilization = some_bank.compute_new_avg_utilization(
indexed_total_deposits,
indexed_total_borrows,
now_ts_i80f48,
);
let price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
some_bank.oracle_config.conf_filter,
some_bank.mint_decimals,
)?;
emit!(UpdateIndexLog {
mango_group: mint_info.group.key(),
token_index: mint_info.token_index,
deposit_index: deposit_index.to_bits(),
borrow_index: borrow_index.to_bits(),
avg_utilization: new_avg_utilization.to_bits(),
price: price.to_bits()
});
drop(some_bank);
msg!("indexed_total_deposits {}", indexed_total_deposits);
msg!("indexed_total_borrows {}", indexed_total_borrows);
msg!("diff_ts {}", diff_ts);
msg!("deposit_index {}", deposit_index);
msg!("borrow_index {}", borrow_index);
msg!("avg_utilization {}", new_avg_utilization);
for ai in ctx.remaining_accounts.iter() {
let mut bank = ai.load_mut::<Bank>()?;
bank.cached_indexed_total_deposits = indexed_total_deposits;
bank.cached_indexed_total_borrows = indexed_total_borrows;
bank.index_last_updated = now_ts;
bank.charge_loan_fee(diff_ts);
bank.deposit_index = deposit_index;
bank.borrow_index = borrow_index;
bank.avg_utilization = new_avg_utilization;
}
}
// compute optimal rates, and max rate and set them on the bank
{
let some_bank = ctx.remaining_accounts[0].load::<Bank>()?;
let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated);
// update each hour
if diff_ts > HOUR {
let (rate0, rate1, max_rate) = some_bank.compute_rates();
emit!(UpdateRateLog {
mango_group: mint_info.group.key(),
token_index: mint_info.token_index,
rate0: rate0.to_bits(),
rate1: rate1.to_bits(),
max_rate: max_rate.to_bits(),
});
drop(some_bank);
msg!("rate0 {}", rate0);
msg!("rate1 {}", rate1);
msg!("max_rate {}", max_rate);
for ai in ctx.remaining_accounts.iter() {
let mut bank = ai.load_mut::<Bank>()?;
bank.bank_rate_last_updated = now_ts;
bank.rate0 = rate0;
bank.rate1 = rate1;
bank.max_rate = max_rate;
}
}
}
Ok(())
}

View File

@ -8,6 +8,7 @@ use fixed::types::I80F48;
use crate::logs::{TokenBalanceLog, WithdrawLog};
use crate::state::new_fixed_order_account_retriever;
use crate::util::checked_math as cm;
#[derive(Accounts)]
pub struct TokenWithdraw<'info> {
@ -16,6 +17,7 @@ pub struct TokenWithdraw<'info> {
#[account(
mut,
has_one = group,
// note: should never be the delegate
has_one = owner,
)]
pub account: AccountLoader<'info, MangoAccount>,
@ -56,21 +58,21 @@ impl<'info> TokenWithdraw<'info> {
// right index for the mint.
// TODO: https://github.com/blockworks-foundation/mango-v4/commit/15961ec81c7e9324b37d79d0e2a1650ce6bd981d comments
pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
require!(amount > 0, MangoError::SomeError);
require_msg!(amount > 0, "withdraw amount must be positive");
let group = ctx.accounts.group.load()?;
let token_index = ctx.accounts.bank.load()?.token_index;
// 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)?;
// The bank will also be passed in remainingAccounts. Use an explicit scope
// to drop the &mut before we borrow it immutably again later.
let position_is_active = {
let (position_is_active, amount_i80f48) = {
let mut bank = ctx.accounts.bank.load_mut()?;
let native_position = position.native(&bank);
@ -104,7 +106,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
amount,
)?;
position_is_active
(position_is_active, amount_i80f48)
};
let indexed_position = position.indexed_position;
@ -112,6 +114,10 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?;
let (bank, oracle_price) =
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
account.net_deposits -= cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::<f32>();
emit!(TokenBalanceLog {
mango_account: ctx.accounts.account.key(),
token_index: token_index,
@ -124,7 +130,8 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
//
// Health check
//
let health = compute_health(&account, HealthType::Init, &retriever)?;
let health = compute_health(&account, HealthType::Init, &retriever)
.context("post-withdraw init health")?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);

View File

@ -1,98 +0,0 @@
use anchor_lang::prelude::*;
use crate::logs::UpdateIndexLog;
use crate::{
accounts_zerocopy::{LoadMutZeroCopyRef, LoadZeroCopyRef},
error::MangoError,
state::{Bank, MintInfo},
};
use checked_math as cm;
use fixed::types::I80F48;
#[derive(Accounts)]
pub struct UpdateIndex<'info> {
pub mint_info: AccountLoader<'info, MintInfo>,
}
pub fn update_index(ctx: Context<UpdateIndex>) -> Result<()> {
let mint_info = ctx.accounts.mint_info.load()?;
let total_banks = mint_info
.banks
.iter()
.filter(|bank| *bank != &Pubkey::default())
.count();
require_eq!(total_banks, ctx.remaining_accounts.len());
let all_banks = ctx.remaining_accounts;
check_banks(all_banks, &mint_info)?;
let mut indexed_total_deposits = I80F48::ZERO;
let mut indexed_total_borrows = I80F48::ZERO;
for ai in all_banks.iter() {
let bank = ai.load::<Bank>()?;
indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits);
indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows);
}
let now_ts = Clock::get()?.unix_timestamp;
let (diff_ts, deposit_index, borrow_index) = {
let mut some_bank = all_banks[0].load_mut::<Bank>()?;
// TODO: should we enforce a minimum window between 2 update_index ix calls?
let diff_ts = I80F48::from_num(now_ts - some_bank.last_updated);
let (deposit_index, borrow_index) =
some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?;
(diff_ts, deposit_index, borrow_index)
};
msg!("indexed_total_deposits {}", indexed_total_deposits);
msg!("indexed_total_borrows {}", indexed_total_borrows);
msg!("diff_ts {}", diff_ts);
msg!("deposit_index {}", deposit_index);
msg!("borrow_index {}", borrow_index);
for ai in all_banks.iter() {
let mut bank = ai.load_mut::<Bank>()?;
bank.cached_indexed_total_deposits = indexed_total_deposits;
bank.cached_indexed_total_borrows = indexed_total_borrows;
bank.last_updated = now_ts;
bank.charge_loan_fee(diff_ts);
bank.deposit_index = deposit_index;
bank.borrow_index = borrow_index;
// clarkeni TODO: add prices
emit!(UpdateIndexLog {
mango_group: bank.group.key(),
token_index: bank.token_index,
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
// price: oracle_price.to_bits(),
});
}
Ok(())
}
fn check_banks(all_banks: &[AccountInfo], mint_info: &MintInfo) -> Result<()> {
for (idx, ai) in all_banks.iter().enumerate() {
match ai.load::<Bank>() {
Ok(bank) => {
if mint_info.token_index != bank.token_index
|| mint_info.group != bank.group
// todo: just below check should be enough, above 2 checks are superfluous and defensive
|| mint_info.banks[idx] != ai.key()
{
return Err(error!(MangoError::SomeError));
}
}
Err(error) => return Err(error),
}
}
Ok(())
}

View File

@ -30,12 +30,12 @@ pub mod mango_v4 {
use super::*;
pub fn create_group(ctx: Context<CreateGroup>, group_num: u32, testing: u8) -> Result<()> {
instructions::create_group(ctx, group_num, testing)
pub fn group_create(ctx: Context<GroupCreate>, group_num: u32, testing: u8) -> Result<()> {
instructions::group_create(ctx, group_num, testing)
}
pub fn close_group(ctx: Context<CloseGroup>) -> Result<()> {
instructions::close_group(ctx)
pub fn group_close(ctx: Context<GroupClose>) -> Result<()> {
instructions::group_close(ctx)
}
#[allow(clippy::too_many_arguments)]
@ -71,6 +71,37 @@ pub mod mango_v4 {
)
}
#[allow(clippy::too_many_arguments)]
pub fn token_edit(
ctx: Context<TokenEdit>,
bank_num: u64,
oracle_opt: Option<Pubkey>,
oracle_config_opt: Option<OracleConfig>,
interest_rate_params_opt: Option<InterestRateParams>,
loan_fee_rate_opt: Option<f32>,
loan_origination_fee_rate_opt: Option<f32>,
maint_asset_weight_opt: Option<f32>,
init_asset_weight_opt: Option<f32>,
maint_liab_weight_opt: Option<f32>,
init_liab_weight_opt: Option<f32>,
liquidation_fee_opt: Option<f32>,
) -> Result<()> {
instructions::token_edit(
ctx,
bank_num,
oracle_opt,
oracle_config_opt,
interest_rate_params_opt,
loan_fee_rate_opt,
loan_origination_fee_rate_opt,
maint_asset_weight_opt,
init_asset_weight_opt,
maint_liab_weight_opt,
init_liab_weight_opt,
liquidation_fee_opt,
)
}
#[allow(clippy::too_many_arguments)]
pub fn token_add_bank(
ctx: Context<TokenAddBank>,
@ -87,22 +118,28 @@ pub mod mango_v4 {
instructions::token_deregister(ctx, token_index)
}
pub fn update_index(ctx: Context<UpdateIndex>) -> Result<()> {
instructions::update_index(ctx)
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
instructions::token_update_index_and_rate(ctx)
}
pub fn create_account(
ctx: Context<CreateAccount>,
pub fn account_create(
ctx: Context<AccountCreate>,
account_num: u8,
name: String,
) -> Result<()> {
instructions::create_account(ctx, account_num, name)
instructions::account_create(ctx, account_num, name)
}
// TODO set delegate
pub fn account_edit(
ctx: Context<AccountEdit>,
name_opt: Option<String>,
delegate_opt: Option<Pubkey>,
) -> Result<()> {
instructions::account_edit(ctx, name_opt, delegate_opt)
}
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
instructions::close_account(ctx)
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
instructions::account_close(ctx)
}
// todo:
@ -110,22 +147,24 @@ pub mod mango_v4 {
// because generic anchor clients won't know how to deal with it
// and it's tricky to use in typescript generally
// lets do an interface pass later
pub fn create_stub_oracle(ctx: Context<CreateStubOracle>, price: I80F48) -> Result<()> {
instructions::create_stub_oracle(ctx, price)
pub fn stub_oracle_create(ctx: Context<StubOracleCreate>, price: I80F48) -> Result<()> {
instructions::stub_oracle_create(ctx, price)
}
pub fn close_stub_oracle(ctx: Context<CloseStubOracle>) -> Result<()> {
instructions::close_stub_oracle(ctx)
pub fn stub_oracle_close(ctx: Context<StubOracleClose>) -> Result<()> {
instructions::stub_oracle_close(ctx)
}
pub fn set_stub_oracle(ctx: Context<SetStubOracle>, price: I80F48) -> Result<()> {
instructions::set_stub_oracle(ctx, price)
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
instructions::stub_oracle_set(ctx, price)
}
// NOTE: keep disc synced in token_update_index_and_rate ix
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
instructions::token_deposit(ctx, amount)
}
// NOTE: keep disc synced in token_update_index_and_rate ix
pub fn token_withdraw(
ctx: Context<TokenWithdraw>,
amount: u64,
@ -162,6 +201,7 @@ pub mod mango_v4 {
instructions::flash_loan3_begin(ctx, loan_amounts)
}
// NOTE: keep disc synced in flash_loan3.rs
pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>,
) -> Result<()> {
@ -182,6 +222,11 @@ pub mod mango_v4 {
instructions::serum3_register_market(ctx, market_index, name)
}
// note:
// pub fn serum3_edit_market - doesn't exist since a mango serum3 market only contains the properties
// registered base and quote token pairs, and serum3 external market its pointing to, and none of them
// should be edited once set on creation
pub fn serum3_deregister_market(ctx: Context<Serum3DeregisterMarket>) -> Result<()> {
instructions::serum3_deregister_market(ctx)
}
@ -260,6 +305,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
///
@ -272,7 +325,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,
@ -293,7 +345,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,
@ -309,6 +360,43 @@ pub mod mango_v4 {
)
}
#[allow(clippy::too_many_arguments)]
pub fn perp_edit_market(
ctx: Context<PerpEditMarket>,
oracle_opt: Option<Pubkey>,
oracle_config_opt: Option<OracleConfig>,
base_token_index_opt: Option<TokenIndex>,
base_token_decimals_opt: Option<u8>,
maint_asset_weight_opt: Option<f32>,
init_asset_weight_opt: Option<f32>,
maint_liab_weight_opt: Option<f32>,
init_liab_weight_opt: Option<f32>,
liquidation_fee_opt: Option<f32>,
maker_fee_opt: Option<f32>,
taker_fee_opt: Option<f32>,
min_funding_opt: Option<f32>,
max_funding_opt: Option<f32>,
impact_quantity_opt: Option<i64>,
) -> Result<()> {
instructions::perp_edit_market(
ctx,
oracle_opt,
oracle_config_opt,
base_token_index_opt,
base_token_decimals_opt,
maint_asset_weight_opt,
init_asset_weight_opt,
maint_liab_weight_opt,
init_liab_weight_opt,
liquidation_fee_opt,
maker_fee_opt,
taker_fee_opt,
min_funding_opt,
max_funding_opt,
impact_quantity_opt,
)
}
pub fn perp_close_market(ctx: Context<PerpCloseMarket>) -> Result<()> {
instructions::perp_close_market(ctx)
}

View File

@ -130,9 +130,19 @@ pub struct UpdateFundingLog {
pub struct UpdateIndexLog {
pub mango_group: Pubkey,
pub token_index: u16,
pub deposit_index: i128, // I80F48
pub borrow_index: i128, // I80F48
// pub price: i128, // I80F48
pub deposit_index: i128, // I80F48
pub borrow_index: i128, // I80F48
pub avg_utilization: i128, // I80F48
pub price: i128, // I80F48
}
#[event]
pub struct UpdateRateLog {
pub mango_group: Pubkey,
pub token_index: u16,
pub rate0: i128, // I80F48
pub rate1: i128, // I80F48
pub max_rate: i128, // I80F48
}
#[event]

View File

@ -1,5 +1,4 @@
use super::{OracleConfig, TokenIndex, TokenPosition};
use crate::error::MangoError;
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
@ -8,14 +7,18 @@ use static_assertions::const_assert_eq;
use std::mem::size_of;
pub const DAY: I80F48 = I80F48!(86400);
pub const YEAR: I80F48 = I80F48!(31536000);
pub const HOUR: i64 = 3600;
pub const DAY: i64 = 86400;
pub const DAY_I80F48: I80F48 = I80F48!(86400);
pub const YEAR_I80F48: 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,
@ -27,17 +30,30 @@ pub struct Bank {
pub deposit_index: I80F48,
pub borrow_index: I80F48,
/// total deposits/borrows, only updated during UpdateIndex
/// TODO: These values could be dropped from the bank, they're written in UpdateIndex
/// total deposits/borrows, only updated during UpdateIndexAndRate
/// TODO: These values could be dropped from the bank, they're written in UpdateIndexAndRate
/// and never read.
pub cached_indexed_total_deposits: I80F48,
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_and_rate).
pub indexed_deposits: I80F48,
pub indexed_borrows: I80F48,
pub last_updated: i64,
pub index_last_updated: i64,
pub bank_rate_last_updated: i64,
pub avg_utilization: I80F48,
pub adjustment_factor: I80F48,
pub util0: I80F48,
pub rate0: I80F48,
pub util1: I80F48,
@ -83,7 +99,7 @@ pub struct Bank {
}
const_assert_eq!(
size_of::<Bank>(),
16 + 32 * 4 + 8 + 16 * 21 + 2 * 8 + 2 + 1 + 1 + 4 + 8
16 + 32 * 4 + 8 * 2 + 16 * 23 + 2 * 8 + 2 + 1 + 1 + 4 + 8
);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -108,7 +124,9 @@ impl std::fmt::Debug for Bank {
)
.field("indexed_deposits", &self.indexed_deposits)
.field("indexed_borrows", &self.indexed_borrows)
.field("last_updated", &self.last_updated)
.field("index_last_updated", &self.index_last_updated)
.field("bank_rate_last_updated", &self.bank_rate_last_updated)
.field("avg_utilization", &self.avg_utilization)
.field("util0", &self.util0)
.field("rate0", &self.rate0)
.field("util1", &self.util1)
@ -149,7 +167,10 @@ impl Bank {
cached_indexed_total_borrows: existing_bank.cached_indexed_total_borrows,
indexed_deposits: I80F48::ZERO,
indexed_borrows: I80F48::ZERO,
last_updated: existing_bank.last_updated,
index_last_updated: existing_bank.index_last_updated,
bank_rate_last_updated: existing_bank.bank_rate_last_updated,
avg_utilization: existing_bank.avg_utilization,
adjustment_factor: existing_bank.adjustment_factor,
util0: existing_bank.util0,
rate0: existing_bank.rate0,
util1: existing_bank.util1,
@ -197,17 +218,31 @@ impl Bank {
position: &mut TokenPosition,
mut native_amount: I80F48,
) -> Result<bool> {
require!(native_amount >= 0, MangoError::SomeError);
require_gte!(native_amount, 0);
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 +262,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);
@ -269,7 +301,7 @@ impl Bank {
mut native_amount: I80F48,
with_loan_origination_fee: bool,
) -> Result<bool> {
require!(native_amount >= 0, MangoError::SomeError);
require_gte!(native_amount, 0);
let native_position = position.native(self);
if native_position.is_positive() {
@ -299,7 +331,9 @@ impl Bank {
}
if with_loan_origination_fee {
self.charge_loan_origination_fee(position, native_amount)?;
let loan_origination_fee = cm!(self.loan_origination_fee_rate * native_amount);
self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee);
native_amount = cm!(native_amount + loan_origination_fee);
}
// add to borrows
@ -310,21 +344,17 @@ impl Bank {
Ok(true)
}
// charge only loan origination fee, assuming borrow has already happened
pub fn charge_loan_origination_fee(
// withdraw the loan origination fee for a borrow that happenend earlier
pub fn withdraw_loan_origination_fee(
&mut self,
position: &mut TokenPosition,
already_borrowed_native_amount: I80F48,
) -> Result<()> {
) -> Result<bool> {
let loan_origination_fee =
cm!(self.loan_origination_fee_rate * already_borrowed_native_amount);
self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee);
let indexed_change = cm!(loan_origination_fee / self.borrow_index);
self.indexed_borrows = cm!(self.indexed_borrows + indexed_change);
position.indexed_position = cm!(position.indexed_position - indexed_change);
Ok(())
self.withdraw_internal(position, loan_origination_fee, false)
}
/// Change a position without applying the loan origination fee
@ -357,31 +387,32 @@ impl Bank {
pub fn charge_loan_fee(&mut self, diff_ts: I80F48) {
let native_borrows_old = self.native_borrows();
self.indexed_borrows =
cm!((self.indexed_borrows * (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR))));
cm!((self.indexed_borrows
* (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR_I80F48))));
self.collected_fees_native =
cm!(self.collected_fees_native + self.native_borrows() - native_borrows_old);
}
pub fn compute_index(
&mut self,
&self,
indexed_total_deposits: I80F48,
indexed_total_borrows: I80F48,
diff_ts: I80F48,
) -> Result<(I80F48, I80F48)> {
// compute index based on utilization
let native_total_deposits = self.deposit_index * indexed_total_deposits;
let native_total_borrows = self.borrow_index * indexed_total_borrows;
let native_total_deposits = cm!(self.deposit_index * indexed_total_deposits);
let native_total_borrows = cm!(self.borrow_index * indexed_total_borrows);
let utilization = if native_total_deposits == I80F48::ZERO {
let instantaneous_utilization = if native_total_deposits == I80F48::ZERO {
I80F48::ZERO
} else {
cm!(native_total_borrows / native_total_deposits)
};
let interest_rate = self.compute_interest_rate(utilization);
let borrow_interest_rate = self.compute_interest_rate(instantaneous_utilization);
let borrow_interest: I80F48 = cm!(interest_rate * diff_ts);
let deposit_interest = cm!(borrow_interest * utilization);
let borrow_interest: I80F48 = cm!(borrow_interest_rate * diff_ts);
let deposit_interest = cm!(borrow_interest * instantaneous_utilization);
// msg!("utilization {}", utilization);
// msg!("interest_rate {}", interest_rate);
@ -392,9 +423,10 @@ impl Bank {
return Ok((self.deposit_index, self.borrow_index));
}
let borrow_index = cm!((self.borrow_index * borrow_interest) / YEAR + self.borrow_index);
let borrow_index =
cm!((self.borrow_index * borrow_interest) / YEAR_I80F48 + self.borrow_index);
let deposit_index =
cm!((self.deposit_index * deposit_interest) / YEAR + self.deposit_index);
cm!((self.deposit_index * deposit_interest) / YEAR_I80F48 + self.deposit_index);
Ok((deposit_index, borrow_index))
}
@ -423,7 +455,6 @@ impl Bank {
rate1: I80F48,
max_rate: I80F48,
) -> I80F48 {
// TODO: daffy: use optimal interest from oracle
if utilization <= util0 {
let slope = cm!(rate0 / util0);
cm!(slope * utilization)
@ -437,6 +468,50 @@ impl Bank {
cm!(rate1 + slope * extra_util)
}
}
// compute new avg utilization
pub fn compute_new_avg_utilization(
&self,
indexed_total_deposits: I80F48,
indexed_total_borrows: I80F48,
now_ts: I80F48,
) -> I80F48 {
if now_ts == I80F48::ZERO {
return I80F48::ZERO;
}
let native_total_deposits = self.deposit_index * indexed_total_deposits;
let native_total_borrows = self.borrow_index * indexed_total_borrows;
let instantaneous_utilization = if native_total_deposits == I80F48::ZERO {
I80F48::ZERO
} else {
cm!(native_total_borrows / native_total_deposits)
};
// combine old and new with relevant factors to form new avg_utilization
// scaling factor for previous avg_utilization is old_ts/new_ts
// scaling factor for instantaneous utilization is (new_ts - old_ts) / new_ts
let bank_rate_last_updated_i80f48 = I80F48::from_num(self.bank_rate_last_updated);
(self.avg_utilization * bank_rate_last_updated_i80f48
+ instantaneous_utilization * (now_ts - bank_rate_last_updated_i80f48))
/ now_ts
}
// computes new optimal rates and max rate
pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) {
// since we have 3 interest rate legs, consider the middle point of the middle leg as the optimal util
let optimal_util = (self.util0 + self.util1) / 2;
// use avg_utilization and not instantaneous_utilization so that rates cannot be manupulated easily
let util_diff = self.avg_utilization - optimal_util;
// move rates up when utilization is above optimal utilization, and vice versa
let adjustment = I80F48::ONE + self.adjustment_factor * util_diff;
// irrespective of which leg current utilization is in, update all rates
(
cm!(self.rate0 * adjustment),
cm!(self.rate1 * adjustment),
cm!(self.max_rate * adjustment),
)
}
}
#[macro_export]
@ -565,4 +640,34 @@ mod tests {
}
Ok(())
}
#[test]
fn test_compute_new_avg_utilization() {
let mut bank = Bank::zeroed();
bank.deposit_index = I80F48::from_num(1.0);
bank.borrow_index = I80F48::from_num(1.0);
bank.bank_rate_last_updated = 0;
let compute_new_avg_utilization_runner =
|bank: &mut Bank, utilization: I80F48, now_ts: i64| {
bank.avg_utilization = bank.compute_new_avg_utilization(
I80F48::ONE,
utilization,
I80F48::from_num(now_ts),
);
bank.bank_rate_last_updated = now_ts;
};
compute_new_avg_utilization_runner(&mut bank, I80F48::ZERO, 0);
assert_eq!(bank.avg_utilization, I80F48::ZERO);
compute_new_avg_utilization_runner(&mut bank, I80F48::from_num(0.5), 10);
assert!((bank.avg_utilization - I80F48::from_num(0.5)).abs() < 0.0001);
compute_new_avg_utilization_runner(&mut bank, I80F48::from_num(0.8), 15);
assert!((bank.avg_utilization - I80F48::from_num(0.6)).abs() < 0.0001);
compute_new_avg_utilization_runner(&mut bank, I80F48::ONE, 20);
assert!((bank.avg_utilization - I80F48::from_num(0.7)).abs() < 0.0001);
}
}

View File

@ -4,22 +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,16 +1,21 @@
use anchor_lang::prelude::*;
use anchor_lang::ZeroCopy;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use serum_dex::state::OpenOrders;
use std::cell::Ref;
use std::collections::HashMap;
use crate::accounts_zerocopy::*;
use crate::error::MangoError;
use crate::error::*;
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
@ -62,7 +67,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
let expected_ais = cm!(active_token_len * 2 // banks + oracles
+ active_perp_len // PerpMarkets
+ active_serum3_len); // open_orders
require_eq!(ais.len(), expected_ais, MangoError::SomeError);
require_eq!(ais.len(), expected_ais);
Ok(FixedOrderAccountRetriever {
ais: ais
@ -76,11 +81,22 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
}
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
fn bank(&self, group: &Pubkey, account_index: usize) -> Result<&Bank> {
fn bank(&self, group: &Pubkey, account_index: usize, token_index: TokenIndex) -> Result<&Bank> {
let bank = self.ais[account_index].load::<Bank>()?;
require!(&bank.group == group, MangoError::SomeError);
require_keys_eq!(bank.group, *group);
require_eq!(bank.token_index, token_index);
Ok(bank)
}
fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
let oracle = &self.ais[cm!(self.n_banks + account_index)];
require_keys_eq!(bank.oracle, *oracle.key());
Ok(oracle_price(
oracle,
bank.oracle_config.conf_filter,
bank.mint_decimals,
)?)
}
}
impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
@ -90,14 +106,27 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
account_index: usize,
token_index: TokenIndex,
) -> Result<(&Bank, I80F48)> {
let bank = self.bank(group, account_index)?;
require_eq!(bank.token_index, token_index, MangoError::SomeError);
let oracle = &self.ais[cm!(self.n_banks + account_index)];
require_keys_eq!(bank.oracle, *oracle.key(), MangoError::SomeError);
Ok((
bank,
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?,
))
let bank = self
.bank(group, account_index, token_index)
.with_context(|| {
format!(
"loading bank with health account index {}, token index {}, passed account {}",
account_index,
token_index,
self.ais[account_index].key(),
)
})?;
let oracle_price = self.oracle_price(account_index, bank).with_context(|| {
format!(
"getting oracle for bank with health account index {} and token index {}, passed account {}",
account_index,
token_index,
self.ais[self.n_banks + account_index].key(),
)
})?;
Ok((bank, oracle_price))
}
fn perp_market(
@ -107,19 +136,35 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
perp_market_index: PerpMarketIndex,
) -> Result<&PerpMarket> {
let ai = &self.ais[cm!(self.begin_perp + account_index)];
let market = ai.load::<PerpMarket>()?;
require!(&market.group == group, MangoError::SomeError);
require!(
market.perp_market_index == perp_market_index,
MangoError::SomeError
);
Ok(market)
(|| {
let market = ai.load::<PerpMarket>()?;
require_keys_eq!(market.group, *group);
require_eq!(market.perp_market_index, perp_market_index);
Ok(market)
})()
.with_context(|| {
format!(
"loading perp market with health account index {} and perp market index {}, passed account {}",
account_index,
perp_market_index,
ai.key(),
)
})
}
fn serum_oo(&self, account_index: usize, key: &Pubkey) -> Result<&OpenOrders> {
let ai = &self.ais[cm!(self.begin_serum3 + account_index)];
require!(key == ai.key(), MangoError::SomeError);
serum3_cpi::load_open_orders(ai)
(|| {
require_keys_eq!(*key, *ai.key());
serum3_cpi::load_open_orders(ai)
})()
.with_context(|| {
format!(
"loading serum open orders with health account index {}, passed account {}",
account_index,
ai.key(),
)
})
}
}
@ -136,47 +181,58 @@ pub struct ScanningAccountRetriever<'a, 'info> {
perp_index_map: HashMap<PerpMarketIndex, usize>,
}
// Returns None if `ai` doesn't have the owner or discriminator for T
fn can_load_as<'a, T: ZeroCopy + Owner>(
(i, ai): (usize, &'a AccountInfo),
) -> Option<(usize, Result<Ref<'a, T>>)> {
let load_result = ai.load::<T>();
match load_result {
Err(Error::AnchorError(error))
if error.error_code_number == ErrorCode::AccountDiscriminatorMismatch as u32
|| error.error_code_number == ErrorCode::AccountOwnedByWrongProgram as u32 =>
{
return None;
}
_ => {}
};
Some((i, load_result))
}
impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
pub fn new(ais: &'a [AccountInfo<'info>], group: &Pubkey) -> Result<Self> {
// find all Bank accounts
let mut token_index_map = HashMap::with_capacity(ais.len() / 2);
for (i, ai) in ais.iter().enumerate() {
match ai.load::<Bank>() {
Ok(bank) => {
require!(&bank.group == group, MangoError::SomeError);
ais.iter()
.enumerate()
.map_while(can_load_as::<Bank>)
.try_for_each(|(i, loaded)| {
(|| {
let bank = loaded?;
require_keys_eq!(bank.group, *group);
token_index_map.insert(bank.token_index, i);
}
Err(Error::AnchorError(error))
if error.error_code_number
== ErrorCode::AccountDiscriminatorMismatch as u32
|| error.error_code_number
== ErrorCode::AccountOwnedByWrongProgram as u32 =>
{
break;
}
Err(error) => return Err(error),
};
}
Ok(())
})()
.with_context(|| format!("scanning banks, health account index {}", i))
})?;
// skip all banks and oracles, then find number of PerpMarket accounts
let skip = token_index_map.len() * 2;
let mut perp_index_map = HashMap::with_capacity(ais.len() - skip);
for (i, ai) in ais[skip..].iter().enumerate() {
match ai.load::<PerpMarket>() {
Ok(perp_market) => {
require!(&perp_market.group == group, MangoError::SomeError);
ais[skip..]
.iter()
.enumerate()
.map_while(can_load_as::<PerpMarket>)
.try_for_each(|(i, loaded)| {
(|| {
let perp_market = loaded?;
require_keys_eq!(perp_market.group, *group);
perp_index_map.insert(perp_market.perp_market_index, cm!(skip + i));
}
Err(Error::AnchorError(error))
if error.error_code_number
== ErrorCode::AccountDiscriminatorMismatch as u32
|| error.error_code_number
== ErrorCode::AccountOwnedByWrongProgram as u32 =>
{
break;
}
Err(error) => return Err(error),
};
}
Ok(())
})()
.with_context(|| {
format!("scanning perp markets, health account index {}", i + skip)
})
})?;
Ok(Self {
ais: ais
@ -201,7 +257,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
Ok(*self
.token_index_map
.get(&token_index)
.ok_or_else(|| error!(MangoError::SomeError))?)
.ok_or_else(|| error_msg!("token index {} not found", token_index))?)
}
#[inline]
@ -209,14 +265,24 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
Ok(*self
.perp_index_map
.get(&perp_market_index)
.ok_or_else(|| error!(MangoError::SomeError))?)
.ok_or_else(|| error_msg!("perp market index {} not found", perp_market_index))?)
}
pub fn banks_mut_and_oracles(
&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_keys_eq!(bank.oracle, *oracle.key);
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 +290,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);
@ -234,16 +299,16 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
let oracle1 = &oracles_part[cm!(n_banks + first - (second + 1))];
let oracle2 = &oracles_part[cm!(n_banks + second - (second + 1))];
require!(&bank1.oracle == oracle1.key, MangoError::SomeError);
require!(&bank2.oracle == oracle2.key, MangoError::SomeError);
require_keys_eq!(bank1.oracle, *oracle1.key);
require_keys_eq!(bank2.oracle, *oracle2.key);
let mint_decimals1 = bank1.mint_decimals;
let mint_decimals2 = bank2.mint_decimals;
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))))
}
}
@ -251,7 +316,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let index = self.bank_index(token_index)?;
let bank = self.ais[index].load_fully_unchecked::<Bank>()?;
let oracle = &self.ais[cm!(self.n_banks() + index)];
require!(&bank.oracle == oracle.key, MangoError::SomeError);
require_keys_eq!(bank.oracle, *oracle.key);
Ok((
bank,
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?,
@ -267,7 +332,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let oo = self.ais[self.begin_serum3()..]
.iter()
.find(|ai| ai.key == key)
.ok_or_else(|| error!(MangoError::SomeError))?;
.ok_or_else(|| error_msg!("no serum3 open orders for key {}", key))?;
serum3_cpi::load_open_orders(oo)
}
}
@ -322,7 +387,7 @@ pub fn compute_health_from_fixed_accounts(
let expected_ais = cm!(active_token_len * 2 // banks + oracles
+ active_perp_len // PerpMarkets
+ active_serum3_len); // open_orders
require!(ais.len() == expected_ais, MangoError::SomeError);
require_eq!(ais.len(), expected_ais);
let retriever = FixedOrderAccountRetriever {
ais: ais
@ -333,7 +398,7 @@ pub fn compute_health_from_fixed_accounts(
begin_perp: cm!(active_token_len * 2),
begin_serum3: cm!(active_token_len * 2 + active_perp_len),
};
new_health_cache(account, &retriever)?.health(health_type)
Ok(new_health_cache(account, &retriever)?.health(health_type))
}
/// Compute health with an arbitrary AccountRetriever
@ -342,10 +407,11 @@ pub fn compute_health(
health_type: HealthType,
retriever: &impl AccountRetriever,
) -> Result<I80F48> {
new_health_cache(account, retriever)?.health(health_type)
Ok(new_health_cache(account, retriever)?.health(health_type))
}
struct TokenInfo {
#[derive(AnchorDeserialize, AnchorSerialize)]
pub struct TokenInfo {
token_index: TokenIndex,
maint_asset_weight: I80F48,
init_asset_weight: I80F48,
@ -374,9 +440,20 @@ impl TokenInfo {
HealthType::Maint => self.maint_liab_weight,
}
}
#[inline(always)]
fn health_contribution(&self, health_type: HealthType) -> I80F48 {
let weight = if self.balance.is_negative() {
self.liab_weight(health_type)
} else {
self.asset_weight(health_type)
};
cm!(self.balance * weight)
}
}
struct Serum3Info {
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct Serum3Info {
reserved: I80F48,
base_index: usize,
quote_index: usize,
@ -421,7 +498,8 @@ impl Serum3Info {
}
}
struct PerpInfo {
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct PerpInfo {
maint_asset_weight: I80F48,
init_asset_weight: I80F48,
maint_liab_weight: I80F48,
@ -460,6 +538,7 @@ impl PerpInfo {
}
}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct HealthCache {
token_infos: Vec<TokenInfo>,
serum3_infos: Vec<Serum3Info>,
@ -467,21 +546,13 @@ pub struct HealthCache {
}
impl HealthCache {
pub fn health(&self, health_type: HealthType) -> Result<I80F48> {
pub fn health(&self, health_type: HealthType) -> I80F48 {
let mut health = I80F48::ZERO;
for token_info in self.token_infos.iter() {
let contrib = health_contribution(health_type, token_info, token_info.balance)?;
let sum = |contrib| {
health = cm!(health + contrib);
}
for serum3_info in self.serum3_infos.iter() {
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
health = cm!(health + contrib);
}
for perp_info in self.perp_infos.iter() {
let contrib = perp_info.health_contribution(health_type);
health = cm!(health + contrib);
}
Ok(health)
};
self.health_sum(health_type, sum);
health
}
pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> {
@ -489,27 +560,80 @@ impl HealthCache {
.token_infos
.iter_mut()
.find(|t| t.token_index == token_index)
.ok_or_else(|| error!(MangoError::SomeError))?;
.ok_or_else(|| error_msg!("token index {} not found", token_index))?;
entry.balance = cm!(entry.balance + change * entry.oracle_price);
Ok(())
}
}
/// Compute health contribution for a given balance
/// wart: independent of the balance stored in TokenInfo
/// balance is in health-reference-token native units
#[inline(always)]
fn health_contribution(
health_type: HealthType,
info: &TokenInfo,
balance: I80F48,
) -> Result<I80F48> {
let weight = if balance.is_negative() {
info.liab_weight(health_type)
} else {
info.asset_weight(health_type)
};
Ok(cm!(balance * weight))
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
}
fn health_sum(&self, health_type: HealthType, mut action: impl FnMut(I80F48)) {
for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type);
action(contrib);
}
for serum3_info in self.serum3_infos.iter() {
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
action(contrib);
}
for perp_info in self.perp_infos.iter() {
let contrib = perp_info.health_contribution(health_type);
action(contrib);
}
}
/// Sum of only the positive health components (assets) and
/// sum of absolute values of all negative health components (liabs, always >= 0)
pub fn health_assets_and_liabs(&self, health_type: HealthType) -> (I80F48, I80F48) {
let mut assets = I80F48::ZERO;
let mut liabs = I80F48::ZERO;
let sum = |contrib| {
if contrib > 0 {
assets = cm!(assets + contrib);
} else {
liabs = cm!(liabs - contrib);
}
};
self.health_sum(health_type, sum);
(assets, liabs)
}
/// The health ratio is
/// - 0 if health is 0 - meaning assets = liabs
/// - 100 if there's 2x as many assets as liabs
/// - 200 if there's 3x as many assets as liabs
/// - MAX if liabs = 0
///
/// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
let (assets, liabs) = self.health_assets_and_liabs(health_type);
let hundred = I80F48::from(100);
if liabs > 0 {
cm!(hundred * (assets - liabs) / liabs)
} else {
I80F48::MAX
}
}
}
/// Generate a HealthCache for an account and its health accounts.
@ -523,7 +647,7 @@ pub fn new_health_cache(
infos
.iter()
.position(|ti| ti.token_index == token_index)
.ok_or_else(|| error!(MangoError::SomeError))
.ok_or_else(|| error_msg!("token index {} not found", token_index))
}
for (i, position) in account.tokens.iter_active().enumerate() {
@ -835,7 +959,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 +1037,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 +1046,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 +1133,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

@ -121,18 +121,26 @@ impl MangoAccountTokenPositions {
}
}
pub fn get(&self, token_index: TokenIndex) -> Result<&TokenPosition> {
/// Returns
/// - the position
/// - the raw index into the token positions list (for use with get_raw/deactivate)
pub fn get(&self, token_index: TokenIndex) -> Result<(&TokenPosition, usize)> {
self.values
.iter()
.find(|p| p.is_active_for_token(token_index))
.ok_or_else(|| error!(MangoError::SomeError)) // TODO: not found error
.enumerate()
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
.ok_or_else(|| error_msg!("position for token index {} not found", token_index))
}
pub fn get_mut(&mut self, token_index: TokenIndex) -> Result<&mut TokenPosition> {
/// Returns
/// - the position
/// - the raw index into the token positions list (for use with get_raw/deactivate)
pub fn get_mut(&mut self, token_index: TokenIndex) -> Result<(&mut TokenPosition, usize)> {
self.values
.iter_mut()
.find(|p| p.is_active_for_token(token_index))
.ok_or_else(|| error!(MangoError::SomeError)) // TODO: not found error
.enumerate()
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
.ok_or_else(|| error_msg!("position for token index {} not found", token_index))
}
pub fn get_mut_raw(&mut self, raw_token_index: usize) -> &mut TokenPosition {
@ -178,7 +186,8 @@ impl MangoAccountTokenPositions {
}
Ok((v, raw_index, bank_index))
} else {
err!(MangoError::SomeError) // TODO: No free space
err!(MangoError::NoFreeTokenPositionIndex)
.context(format!("when looking for token index {}", token_index))
}
}
@ -287,7 +296,7 @@ impl MangoAccountSerum3Orders {
pub fn create(&mut self, market_index: Serum3MarketIndex) -> Result<&mut Serum3Orders> {
if self.find(market_index).is_some() {
return err!(MangoError::SomeError); // exists already
return err!(MangoError::Serum3OpenOrdersExistAlready);
}
if let Some(v) = self.values.iter_mut().find(|p| !p.is_active()) {
*v = Serum3Orders {
@ -296,7 +305,7 @@ impl MangoAccountSerum3Orders {
};
Ok(v)
} else {
err!(MangoError::SomeError) // no space
err!(MangoError::NoFreeSerum3OpenOrdersIndex)
}
}
@ -305,7 +314,7 @@ impl MangoAccountSerum3Orders {
.values
.iter()
.position(|p| p.is_active_for_market(market_index))
.ok_or(MangoError::SomeError)?;
.ok_or_else(|| error_msg!("serum3 open orders index {} not found", market_index))?;
self.values[index].market_index = Serum3MarketIndex::MAX;
@ -541,7 +550,7 @@ impl MangoAccountPerpPositions {
if let Some(i) = pos {
Ok((&mut self.accounts[i], i))
} else {
err!(MangoError::SomeError) // TODO: No free space
err!(MangoError::NoFreePerpPositionIndex)
}
}
@ -587,10 +596,7 @@ impl MangoAccountPerpPositions {
}
pub fn remove_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
require!(
self.order_market[slot] != FREE_ORDER_SLOT,
MangoError::SomeError
);
require_neq!(self.order_market[slot], FREE_ORDER_SLOT);
let order_side = self.order_side[slot];
let perp_market_index = self.order_market[slot];
let perp_account = self.get_account_mut_or_create(perp_market_index).unwrap().0;
@ -704,11 +710,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,16 +732,24 @@ 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,
// pub info: [u8; INFO_LEN], // TODO: Info could be in a separate PDA?
pub reserved: [u8; 4],
// Cumulative (deposits - withdraws)
// using USD prices at the time of the deposit/withdraw
// in UI USD units
pub net_deposits: f32,
// Cumulative settles on perp positions
// TODO: unimplemented
pub net_settled: f32,
}
const_assert_eq!(
size_of::<MangoAccount>(),
@ -742,6 +759,7 @@ const_assert_eq!(
+ size_of::<MangoAccountPerpPositions>()
+ 4
+ 4
+ 2 * 4 // net_deposits and net_settled
);
const_assert_eq!(size_of::<MangoAccount>() % 8, 0);
@ -770,6 +788,26 @@ impl MangoAccount {
.unwrap()
.trim_matches(char::from(0))
}
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 {
@ -787,6 +825,8 @@ impl Default for MangoAccount {
account_num: 0,
bump: 0,
reserved: Default::default(),
net_deposits: 0.0,
net_settled: 0.0,
}
}
}

View File

@ -2,6 +2,8 @@ use anchor_lang::prelude::*;
use static_assertions::const_assert_eq;
use std::mem::size_of;
use crate::error::*;
use super::TokenIndex;
pub const MAX_BANKS: usize = 6;
@ -13,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);
@ -44,4 +49,25 @@ impl MintInfo {
pub fn first_vault(&self) -> Pubkey {
self.vaults[0]
}
pub fn num_banks(&self) -> usize {
self.banks
.iter()
.position(|&b| b == Pubkey::default())
.unwrap_or(MAX_BANKS)
}
pub fn banks(&self) -> &[Pubkey] {
&self.banks[..self.num_banks()]
}
pub fn verify_banks_ais(&self, all_bank_ais: &[AccountInfo]) -> Result<()> {
require_msg!(
all_bank_ais.iter().map(|ai| ai.key).eq(self.banks().iter()),
"the passed banks {:?} don't match banks in mint_info {:?}",
all_bank_ais.iter().map(|ai| ai.key).collect::<Vec<_>>(),
self.banks()
);
Ok(())
}
}

View File

@ -12,8 +12,6 @@ use crate::accounts_zerocopy::*;
use crate::checked_math as cm;
use crate::error::MangoError;
pub const QUOTE_DECIMALS: i8 = 6;
const LOOKUP_START: i8 = -12;
const LOOKUP: [I80F48; 25] = [
I80F48::from_bits((1 << 48) / 10i128.pow(12u32)),
@ -44,6 +42,9 @@ const LOOKUP: [I80F48; 25] = [
];
const LOOKUP_FN: fn(i8) -> usize = |decimals: i8| (decimals - LOOKUP_START) as usize;
pub const QUOTE_DECIMALS: i8 = 6;
pub const QUOTE_NATIVE_TO_UI: I80F48 = LOOKUP[(-QUOTE_DECIMALS - LOOKUP_START) as usize];
pub mod switchboard_v1_devnet_oracle {
use solana_program::declare_id;
declare_id!("7azgmy1pFXHikv36q1zZASvFq5vFa39TT9NweVugKKTU");
@ -71,7 +72,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

@ -6,19 +6,30 @@ use fixed::types::I80F48;
use static_assertions::const_assert_eq;
use crate::state::orderbook::order_type::Side;
use crate::state::{TokenIndex, DAY};
use crate::state::TokenIndex;
use crate::util::checked_math as cm;
use super::{Book, OracleConfig};
use super::{Book, OracleConfig, DAY_I80F48};
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,
@ -75,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);
@ -129,7 +134,7 @@ impl PerpMarket {
};
let diff_ts = I80F48::from_num(now_ts - self.funding_last_updated as u64);
let time_factor = cm!(diff_ts / DAY);
let time_factor = cm!(diff_ts / DAY_I80F48);
let base_lot_size = I80F48::from_num(self.base_lot_size);
let funding_delta = cm!(index_price * diff_price * base_lot_size * time_factor);

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

@ -13,8 +13,6 @@ use solana_program::instruction::Instruction;
use solana_sdk::instruction;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::transport::TransportError;
use spl_associated_token_account::get_associated_token_address;
use std::str::FromStr;
use std::sync::Arc;
@ -233,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![];
@ -245,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)
@ -257,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);
}
@ -308,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;
@ -629,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> {
@ -661,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,
)
@ -671,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(),
};
@ -688,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(
@ -725,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,
)
@ -734,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(),
@ -748,13 +762,14 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.token_authority]
vec![&self.token_authority]
}
}
pub struct TokenRegisterInstruction<'keypair> {
pub token_index: TokenIndex,
pub decimals: u8,
pub adjustment_factor: f32,
pub util0: f32,
pub rate0: f32,
pub util1: f32,
@ -791,6 +806,7 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> {
conf_filter: I80F48::from_num::<f32>(0.10),
},
interest_rate_params: InterestRateParams {
adjustment_factor: self.adjustment_factor,
util0: self.util0,
rate0: self.rate0,
util1: self.util1,
@ -879,7 +895,6 @@ pub struct TokenAddBankInstruction<'keypair> {
pub group: Pubkey,
pub admin: &'keypair Keypair,
pub mint: Pubkey,
pub address_lookup_table: Pubkey,
pub payer: &'keypair Keypair,
}
@ -889,7 +904,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 {
@ -927,12 +942,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;
@ -940,7 +955,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,
@ -1031,7 +1046,7 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> {
}
}
pub struct SetStubOracleInstruction<'keypair> {
pub struct StubOracleSetInstruction<'keypair> {
pub mint: Pubkey,
pub group: Pubkey,
pub admin: &'keypair Keypair,
@ -1039,9 +1054,9 @@ pub struct SetStubOracleInstruction<'keypair> {
pub price: &'static str,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for SetStubOracleInstruction<'keypair> {
type Accounts = mango_v4::accounts::SetStubOracle;
type Instruction = mango_v4::instruction::SetStubOracle;
impl<'keypair> ClientInstruction for StubOracleSetInstruction<'keypair> {
type Accounts = mango_v4::accounts::StubOracleSet;
type Instruction = mango_v4::instruction::StubOracleSet;
async fn to_instruction(
&self,
@ -1078,16 +1093,16 @@ impl<'keypair> ClientInstruction for SetStubOracleInstruction<'keypair> {
}
}
pub struct CreateStubOracle<'keypair> {
pub struct StubOracleCreate<'keypair> {
pub group: Pubkey,
pub mint: Pubkey,
pub admin: &'keypair Keypair,
pub payer: &'keypair Keypair,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CreateStubOracle<'keypair> {
type Accounts = mango_v4::accounts::CreateStubOracle;
type Instruction = mango_v4::instruction::CreateStubOracle;
impl<'keypair> ClientInstruction for StubOracleCreate<'keypair> {
type Accounts = mango_v4::accounts::StubOracleCreate;
type Instruction = mango_v4::instruction::StubOracleCreate;
async fn to_instruction(
&self,
@ -1126,16 +1141,16 @@ impl<'keypair> ClientInstruction for CreateStubOracle<'keypair> {
}
}
pub struct CloseStubOracleInstruction<'keypair> {
pub struct StubOracleCloseInstruction<'keypair> {
pub group: Pubkey,
pub mint: Pubkey,
pub admin: &'keypair Keypair,
pub sol_destination: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CloseStubOracleInstruction<'keypair> {
type Accounts = mango_v4::accounts::CloseStubOracle;
type Instruction = mango_v4::instruction::CloseStubOracle;
impl<'keypair> ClientInstruction for StubOracleCloseInstruction<'keypair> {
type Accounts = mango_v4::accounts::StubOracleClose;
type Instruction = mango_v4::instruction::StubOracleClose;
async fn to_instruction(
&self,
@ -1171,14 +1186,15 @@ impl<'keypair> ClientInstruction for CloseStubOracleInstruction<'keypair> {
}
}
pub struct CreateGroupInstruction<'keypair> {
pub struct GroupCreateInstruction<'keypair> {
pub admin: &'keypair Keypair,
pub payer: &'keypair Keypair,
pub insurance_mint: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
type Accounts = mango_v4::accounts::CreateGroup;
type Instruction = mango_v4::instruction::CreateGroup;
impl<'keypair> ClientInstruction for GroupCreateInstruction<'keypair> {
type Accounts = mango_v4::accounts::GroupCreate;
type Instruction = mango_v4::instruction::GroupCreate;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
@ -1199,11 +1215,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);
@ -1215,15 +1241,15 @@ impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
}
}
pub struct CloseGroupInstruction<'keypair> {
pub struct GroupCloseInstruction<'keypair> {
pub admin: &'keypair Keypair,
pub group: Pubkey,
pub sol_destination: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CloseGroupInstruction<'keypair> {
type Accounts = mango_v4::accounts::CloseGroup;
type Instruction = mango_v4::instruction::CloseGroup;
impl<'keypair> ClientInstruction for GroupCloseInstruction<'keypair> {
type Accounts = mango_v4::accounts::GroupClose;
type Instruction = mango_v4::instruction::GroupClose;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
@ -1247,7 +1273,7 @@ impl<'keypair> ClientInstruction for CloseGroupInstruction<'keypair> {
}
}
pub struct CreateAccountInstruction<'keypair> {
pub struct AccountCreateInstruction<'keypair> {
pub account_num: u8,
pub group: Pubkey,
@ -1255,15 +1281,15 @@ pub struct CreateAccountInstruction<'keypair> {
pub payer: &'keypair Keypair,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
type Accounts = mango_v4::accounts::CreateAccount;
type Instruction = mango_v4::instruction::CreateAccount;
impl<'keypair> ClientInstruction for AccountCreateInstruction<'keypair> {
type Accounts = mango_v4::accounts::AccountCreate;
type Instruction = mango_v4::instruction::AccountCreate;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = mango_v4::instruction::CreateAccount {
let instruction = mango_v4::instruction::AccountCreate {
account_num: self.account_num,
name: "my_mango_account".to_string(),
};
@ -1279,7 +1305,7 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
)
.0;
let accounts = mango_v4::accounts::CreateAccount {
let accounts = mango_v4::accounts::AccountCreate {
group: self.group,
owner: self.owner.pubkey(),
account,
@ -1296,16 +1322,63 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
}
}
pub struct CloseAccountInstruction<'keypair> {
pub struct AccountEditInstruction<'keypair> {
pub account_num: u8,
pub group: Pubkey,
pub owner: &'keypair Keypair,
pub name: String,
pub delegate: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for AccountEditInstruction<'keypair> {
type Accounts = mango_v4::accounts::AccountEdit;
type Instruction = mango_v4::instruction::AccountEdit;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = mango_v4::instruction::AccountEdit {
name_opt: Option::from(self.name.to_string()),
delegate_opt: Option::from(self.delegate),
};
let account = Pubkey::find_program_address(
&[
self.group.as_ref(),
b"MangoAccount".as_ref(),
self.owner.pubkey().as_ref(),
&self.account_num.to_le_bytes(),
],
&program_id,
)
.0;
let accounts = mango_v4::accounts::AccountEdit {
group: self.group,
account,
owner: self.owner.pubkey(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.owner]
}
}
pub struct AccountCloseInstruction<'keypair> {
pub group: Pubkey,
pub account: Pubkey,
pub owner: &'keypair Keypair,
pub sol_destination: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for CloseAccountInstruction<'keypair> {
type Accounts = mango_v4::accounts::CloseAccount;
type Instruction = mango_v4::instruction::CloseAccount;
impl<'keypair> ClientInstruction for AccountCloseInstruction<'keypair> {
type Accounts = mango_v4::accounts::AccountClose;
type Instruction = mango_v4::instruction::AccountClose;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
@ -1939,7 +2012,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)]
@ -1964,7 +2039,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;
@ -1986,6 +2063,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,
@ -1997,7 +2162,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,
@ -2024,7 +2188,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,
@ -2384,29 +2547,32 @@ impl ClientInstruction for BenchmarkInstruction {
vec![]
}
}
pub struct UpdateIndexInstruction {
pub struct TokenUpdateIndexAndRateInstruction {
pub mint_info: Pubkey,
pub banks: Vec<Pubkey>,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for UpdateIndexInstruction {
type Accounts = mango_v4::accounts::UpdateIndex;
type Instruction = mango_v4::instruction::UpdateIndex;
impl ClientInstruction for TokenUpdateIndexAndRateInstruction {
type Accounts = mango_v4::accounts::TokenUpdateIndexAndRate;
type Instruction = mango_v4::instruction::TokenUpdateIndexAndRate;
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: mint_info.oracle,
instructions: solana_program::sysvar::instructions::id(),
};
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,
GroupCreateInstruction {
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;
@ -45,7 +55,7 @@ impl<'a> GroupWithTokensConfig<'a> {
for (index, mint) in mints.iter().enumerate() {
let create_stub_oracle_accounts = send_tx(
solana,
CreateStubOracle {
StubOracleCreate {
group,
mint: mint.pubkey,
admin,
@ -57,7 +67,7 @@ impl<'a> GroupWithTokensConfig<'a> {
let oracle = create_stub_oracle_accounts.oracle;
send_tx(
solana,
SetStubOracleInstruction {
StubOracleSetInstruction {
group,
admin,
mint: mint.pubkey,
@ -73,6 +83,7 @@ impl<'a> GroupWithTokensConfig<'a> {
TokenRegisterInstruction {
token_index,
decimals: mint.decimals,
adjustment_factor: 0.01,
util0: 0.40,
rate0: 0.07,
util1: 0.80,
@ -94,14 +105,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 +127,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,642 @@
#![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,
AccountCreateInstruction {
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,
AccountCreateInstruction {
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,
StubOracleSetInstruction {
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!(account_position_closed(solana, account, collateral_token1.bank).await);
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!(account_position_closed(solana, account, collateral_token2.bank).await,);
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,
AccountCreateInstruction {
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,
AccountCreateInstruction {
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,
StubOracleSetInstruction {
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!(account_position_closed(solana, account, collateral_token1.bank).await);
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!(account_position_closed(solana, account, collateral_token2.bank).await,);
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,
@ -39,7 +39,7 @@ async fn test_basic() -> Result<(), TransportError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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
@ -80,6 +81,13 @@ async fn test_basic() -> Result<(), TransportError> {
);
let bank_data: Bank = solana.get_account(bank).await;
assert!(bank_data.native_deposits() - I80F48::from_num(deposit_amount) < dust_threshold);
let account_data: MangoAccount = solana.get_account(account).await;
// Assumes oracle price of 1
assert_eq!(
account_data.net_deposits,
(I80F48::from_num(deposit_amount) * QUOTE_NATIVE_TO_UI).to_num::<f32>()
);
}
//
@ -111,6 +119,7 @@ async fn test_basic() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await
@ -130,6 +139,13 @@ async fn test_basic() -> Result<(), TransportError> {
bank_data.native_deposits() - I80F48::from_num(start_amount - withdraw_amount)
< dust_threshold
);
let account_data: MangoAccount = solana.get_account(account).await;
// Assumes oracle price of 1
assert_eq!(
account_data.net_deposits,
(I80F48::from_num(start_amount - withdraw_amount) * QUOTE_NATIVE_TO_UI).to_num::<f32>()
);
}
//
@ -139,12 +155,8 @@ async fn test_basic() -> Result<(), TransportError> {
// withdraw whatever is remaining, can't close bank vault without this
send_tx(
solana,
UpdateIndexInstruction {
TokenUpdateIndexAndRateInstruction {
mint_info: tokens[0].mint_info,
banks: {
let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await;
mint_info.banks.to_vec()
},
},
)
.await
@ -158,6 +170,7 @@ async fn test_basic() -> Result<(), TransportError> {
account,
owner,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await
@ -166,7 +179,7 @@ async fn test_basic() -> Result<(), TransportError> {
// close account
send_tx(
solana,
CloseAccountInstruction {
AccountCloseInstruction {
group,
account,
owner,
@ -204,7 +217,7 @@ async fn test_basic() -> Result<(), TransportError> {
// close stub oracle
send_tx(
solana,
CloseStubOracleInstruction {
StubOracleCloseInstruction {
group,
mint: bank_data.mint,
admin,
@ -217,7 +230,7 @@ async fn test_basic() -> Result<(), TransportError> {
// close group
send_tx(
solana,
CloseGroupInstruction {
GroupCloseInstruction {
group,
admin,
sol_destination: payer.pubkey(),

View File

@ -0,0 +1,151 @@
#![cfg(feature = "test-bpf")]
use solana_program_test::*;
use solana_sdk::{signature::Keypair, signature::Signer, transport::TransportError};
use mango_v4::state::*;
use program_test::*;
mod program_test;
#[tokio::test]
async fn test_delegate() -> 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 delegate = &context.users[1].key;
let mints = &context.mints[0..1];
let payer_mint0_account = context.users[1].token_accounts[0];
//
// SETUP: Create a group, register a token (mint0), create an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let bank = tokens[0].bank;
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
// deposit
send_tx(
solana,
TokenDepositInstruction {
amount: 100,
account,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// TEST: Edit account - Set delegate
//
{
send_tx(
solana,
AccountEditInstruction {
delegate: delegate.pubkey(),
account_num: 0,
group,
owner,
name: "new_name".to_owned(),
},
)
.await
.unwrap();
}
//
// TEST: Edit account as delegate - should fail
//
{
let res = send_tx(
solana,
AccountEditInstruction {
delegate: delegate.pubkey(),
account_num: 0,
group,
owner: delegate,
name: "new_name".to_owned(),
},
)
.await;
assert!(res.is_err());
}
//
// TEST: Withdraw funds as delegate should fail
//
{
let withdraw_amount = 50;
let res = send_tx(
solana,
TokenWithdrawInstruction {
amount: withdraw_amount,
allow_borrow: true,
account,
owner: delegate,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await;
assert!(res.is_err());
}
//
// TEST: Close account as delegate should fail
//
{
let bank_data: Bank = solana.get_account(bank).await;
send_tx(
solana,
TokenWithdrawInstruction {
amount: bank_data.native_deposits().to_num(),
allow_borrow: false,
account,
owner,
token_account: payer_mint0_account,
bank_index: 0,
},
)
.await
.unwrap();
let res = send_tx(
solana,
AccountCloseInstruction {
group,
account,
owner: delegate,
sol_destination: payer.pubkey(),
},
)
.await;
assert!(res.is_err());
}
Ok(())
}

View File

@ -31,14 +31,14 @@ async fn test_group_address_lookup_tables() -> Result<()> {
// SETUP: Create a group
//
let group = send_tx(solana, CreateGroupInstruction { admin, payer })
let group = send_tx(solana, GroupCreateInstruction { admin, payer })
.await
.unwrap()
.group;
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -56,7 +56,7 @@ async fn test_group_address_lookup_tables() -> Result<()> {
let register_mint = |index: TokenIndex, mint: MintCookie, address_lookup_table: Pubkey| async move {
let create_stub_oracle_accounts = send_tx(
solana,
CreateStubOracle {
StubOracleCreate {
mint: mint.pubkey,
payer,
},
@ -66,7 +66,7 @@ async fn test_group_address_lookup_tables() -> Result<()> {
let oracle = create_stub_oracle_accounts.oracle;
send_tx(
solana,
SetStubOracleInstruction {
StubOracleSetInstruction {
group,
admin,
mint: mint.pubkey,

View File

@ -35,7 +35,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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,
@ -98,7 +99,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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,
@ -208,7 +210,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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,
@ -40,7 +40,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
// deposit some funds, to the vaults aren't empty
let vault_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 2,
group,
owner,
@ -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
@ -94,7 +95,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
//
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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
@ -159,7 +161,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
//
send_tx(
solana,
SetStubOracleInstruction {
StubOracleSetInstruction {
group,
admin,
mint: base_token.mint.pubkey,
@ -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,
@ -245,7 +249,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
// deposit some funds, to the vaults aren't empty
let vault_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 2,
group,
owner,
@ -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
@ -274,7 +279,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
//
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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
@ -342,7 +351,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
//
send_tx(
solana,
SetStubOracleInstruction {
StubOracleSetInstruction {
group,
admin,
mint: borrow_token1.mint.pubkey,
@ -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),
},
)
@ -376,12 +387,10 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
account_position(solana, account, borrow_token2.bank).await,
-50 + 19
);
assert_eq!(
account_position(solana, account, collateral_token2.bank).await,
0
);
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
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,22 +405,22 @@ 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
.unwrap();
// the asset cost for 50-19=31 borrow2 is 31 * 1.04 = 32.24
assert_eq!(
account_position(solana, account, borrow_token2.bank).await,
0
);
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
assert_eq!(
account_position(solana, account, collateral_token1.bank).await,
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 +434,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 +451,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 +466,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 +486,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,
@ -51,7 +51,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
let provider_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 1,
group,
owner,
@ -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
@ -91,7 +93,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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,
@ -364,7 +367,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
let provider_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 1,
group,
owner,
@ -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
@ -404,7 +409,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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,
@ -622,7 +628,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
let provider_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 1,
group,
owner,
@ -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
@ -662,7 +670,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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,
@ -34,7 +34,7 @@ async fn test_perp() -> Result<(), TransportError> {
let account_0 = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -47,7 +47,7 @@ async fn test_perp() -> Result<(), TransportError> {
let account_1 = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 1,
group,
owner,
@ -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,
@ -36,7 +36,7 @@ async fn test_position_lifetime() -> Result<()> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -49,7 +49,7 @@ async fn test_position_lifetime() -> Result<()> {
let funding_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 1,
group,
owner,
@ -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,
@ -38,7 +38,7 @@ async fn test_serum() -> Result<(), TransportError> {
let account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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};
@ -9,7 +9,7 @@ use program_test::*;
mod program_test;
#[tokio::test]
async fn test_update_index() -> Result<(), TransportError> {
async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
@ -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,
@ -34,7 +34,7 @@ async fn test_update_index() -> Result<(), TransportError> {
// deposit some funds, to the vaults aren't empty
let deposit_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 0,
group,
owner,
@ -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
@ -60,7 +61,7 @@ async fn test_update_index() -> Result<(), TransportError> {
let withdraw_account = send_tx(
solana,
CreateAccountInstruction {
AccountCreateInstruction {
account_num: 1,
group,
owner,
@ -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,33 +93,36 @@ async fn test_update_index() -> Result<(), TransportError> {
account: withdraw_account,
owner,
token_account: context.users[0].token_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
let bank_before_update_index = solana.get_account::<Bank>(tokens[0].bank).await;
let bank_before_update_index_and_rate = solana.get_account::<Bank>(tokens[0].bank).await;
solana.advance_clock().await;
send_tx(
solana,
UpdateIndexInstruction {
TokenUpdateIndexAndRateInstruction {
mint_info: tokens[0].mint_info,
banks: {
let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await;
mint_info.banks.to_vec()
},
},
)
.await
.unwrap();
let bank_after_update_index = solana.get_account::<Bank>(tokens[0].bank).await;
dbg!(bank_after_update_index);
dbg!(bank_after_update_index);
assert!(bank_before_update_index.deposit_index < bank_after_update_index.deposit_index);
assert!(bank_before_update_index.borrow_index < bank_after_update_index.borrow_index);
let bank_after_update_index_and_rate = solana.get_account::<Bank>(tokens[0].bank).await;
dbg!(bank_after_update_index_and_rate);
dbg!(bank_after_update_index_and_rate);
assert!(
bank_before_update_index_and_rate.deposit_index
< bank_after_update_index_and_rate.deposit_index
);
assert!(
bank_before_update_index_and_rate.borrow_index
< bank_after_update_index_and_rate.borrow_index
);
Ok(())
}

View File

@ -15,12 +15,12 @@ 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
solana --url https://mango.devnet.rpcpool.com program deploy --program-id $PROGRAM_ID \
-k $WALLET_WITH_FUNDS target/deploy/mango_v4.so
-k $WALLET_WITH_FUNDS target/deploy/mango_v4.so --skip-fee-check
# # publish idl
# anchor idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \

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

@ -141,9 +141,39 @@ export class Bank {
}
toString(): string {
return `Bank ${
this.tokenIndex
} deposit index - ${this.depositIndex.toNumber()}, borrow index - ${this.borrowIndex.toNumber()}`;
return (
'Bank ' +
'\n token index -' +
this.tokenIndex +
'\n deposit index -' +
this.depositIndex.toNumber() +
'\n borrow index -' +
this.borrowIndex.toNumber() +
'\n cachedIndexedTotalDeposits -' +
this.cachedIndexedTotalDeposits.toNumber() +
'\n cachedIndexedTotalBorrows -' +
this.cachedIndexedTotalBorrows.toNumber() +
'\n maxRate -' +
this.maxRate.toNumber() +
'\n util0 -' +
this.util0.toNumber() +
'\n rate0 -' +
this.rate0.toNumber() +
'\n util1 -' +
this.util1.toNumber() +
'\n rate1 -' +
this.rate1.toNumber() +
'\n maintAssetWeight -' +
this.maintAssetWeight.toNumber() +
'\n initAssetWeight -' +
this.initAssetWeight.toNumber() +
'\n maintLiabWeight -' +
this.maintLiabWeight.toNumber() +
'\n initLiabWeight -' +
this.initLiabWeight.toNumber() +
'\n liquidationFee -' +
this.liquidationFee.toNumber()
);
}
nativeDeposits(): I80F48 {

View File

@ -171,7 +171,6 @@ export class Group {
public async reloadBankPrices(client: MangoClient, ids?: Id): Promise<void> {
const banks = Array.from(this?.banksMap, ([, value]) => value);
const oracles = banks.map((b) => b.oracle);
console.log(oracles.toString());
const prices =
await client.program.provider.connection.getMultipleAccountsInfo(oracles);

View File

@ -0,0 +1,224 @@
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
import { HealthType } from './mangoAccount';
// ░░░░
//
// ██
// ██░░██
// ░░ ░░ ██░░░░░░██ ░░░░
// ██░░░░░░░░░░██
// ██░░░░░░░░░░██
// ██░░░░░░░░░░░░░░██
// ██░░░░░░██████░░░░░░██
// ██░░░░░░██████░░░░░░██
// ██░░░░░░░░██████░░░░░░░░██
// ██░░░░░░░░██████░░░░░░░░██
// ██░░░░░░░░░░██████░░░░░░░░░░██
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
// ██░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░██
// ░░ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
// ██████████████████████████████████████████
// warning: this code is copy pasta from rust, keep in sync with health.rs
export class HealthCache {
tokenInfos: TokenInfo[];
serum3Infos: Serum3Info[];
perpInfos: PerpInfo[];
constructor(dto: HealthCacheDto) {
this.tokenInfos = dto.tokenInfos.map((dto) => new TokenInfo(dto));
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto));
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto));
}
public health(healthType: HealthType): I80F48 {
let health = ZERO_I80F48;
for (const tokenInfo of this.tokenInfos) {
let contrib = tokenInfo.healthContribution(healthType);
health = health.add(contrib);
}
for (const serum3Info of this.serum3Infos) {
let contrib = serum3Info.healthContribution(healthType, this.tokenInfos);
health = health.add(contrib);
}
for (const perpInfo of this.perpInfos) {
let contrib = perpInfo.healthContribution(healthType);
health = health.add(contrib);
}
return health;
}
}
export class TokenInfo {
constructor(dto: TokenInfoDto) {
this.tokenIndex = dto.tokenIndex;
this.maintAssetWeight = I80F48.from(dto.maintAssetWeight);
this.initAssetWeight = I80F48.from(dto.initAssetWeight);
this.maintLiabWeight = I80F48.from(dto.maintLiabWeight);
this.initLiabWeight = I80F48.from(dto.initLiabWeight);
this.oraclePrice = I80F48.from(dto.oraclePrice);
this.balance = I80F48.from(dto.balance);
this.serum3MaxReserved = I80F48.from(dto.serum3MaxReserved);
}
tokenIndex: number;
maintAssetWeight: I80F48;
initAssetWeight: I80F48;
maintLiabWeight: I80F48;
initLiabWeight: I80F48;
oraclePrice: I80F48; // native/native
// in health-reference-token native units
balance: I80F48;
// in health-reference-token native units
serum3MaxReserved: I80F48;
assetWeight(healthType: HealthType): I80F48 {
return healthType == HealthType.init
? this.initAssetWeight
: this.maintAssetWeight;
}
liabWeight(healthType: HealthType): I80F48 {
return healthType == HealthType.init
? this.initLiabWeight
: this.maintLiabWeight;
}
healthContribution(healthType: HealthType): I80F48 {
return (
this.balance.isNeg()
? this.liabWeight(healthType)
: this.assetWeight(healthType)
).mul(this.balance);
}
}
export class Serum3Info {
constructor(dto: Serum3InfoDto) {
this.reserved = I80F48.from(dto.reserved);
this.baseIndex = dto.baseIndex;
this.quoteIndex = dto.quoteIndex;
}
reserved: I80F48;
baseIndex: number;
quoteIndex: number;
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
let baseInfo = tokenInfos[this.baseIndex];
let quoteInfo = tokenInfos[this.quoteIndex];
let reserved = this.reserved;
if (reserved.isZero()) {
return ZERO_I80F48;
}
// How much the health would increase if the reserved balance were applied to the passed
// token info?
let computeHealthEffect = function (tokenInfo: TokenInfo) {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `reserved` is already included in `max_balance`.
let maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved);
// Assuming `reserved` was added to `max_balance` last (because that gives the smallest
// health effects): how much did health change because of it?
let assetPart, liabPart;
if (maxBalance.gte(reserved)) {
assetPart = reserved;
liabPart = ZERO_I80F48;
} else if (maxBalance.isNeg()) {
assetPart = ZERO_I80F48;
liabPart = reserved;
} else {
assetPart = maxBalance;
liabPart = reserved.sub(maxBalance);
}
let assetWeight = tokenInfo.assetWeight(healthType);
let liabWeight = tokenInfo.liabWeight(healthType);
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
};
let reservedAsBase = computeHealthEffect(baseInfo);
let reservedAsQuote = computeHealthEffect(quoteInfo);
return reservedAsBase.min(reservedAsQuote);
}
}
export class PerpInfo {
constructor(dto: PerpInfoDto) {
this.maintAssetWeight = I80F48.from(dto.maintAssetWeight);
this.initAssetWeight = I80F48.from(dto.initAssetWeight);
this.maintLiabWeight = I80F48.from(dto.maintLiabWeight);
this.initLiabWeight = I80F48.from(dto.initLiabWeight);
this.base = I80F48.from(dto.base);
this.quote = I80F48.from(dto.quote);
}
maintAssetWeight: I80F48;
initAssetWeight: I80F48;
maintLiabWeight: I80F48;
initLiabWeight: I80F48;
// in health-reference-token native units, needs scaling by asset/liab
base: I80F48;
// in health-reference-token native units, no asset/liab factor needed
quote: I80F48;
healthContribution(healthType: HealthType): I80F48 {
let weight;
if (healthType == HealthType.init && this.base.isNeg()) {
weight = this.initLiabWeight;
} else if (healthType == HealthType.init && !this.base.isNeg()) {
weight = this.initAssetWeight;
}
if (healthType == HealthType.maint && this.base.isNeg()) {
weight = this.maintLiabWeight;
}
if (healthType == HealthType.maint && !this.base.isNeg()) {
weight = this.maintAssetWeight;
}
// FUTURE: Allow v3-style "reliable" markets where we can return
// `self.quote + weight * self.base` here
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48);
}
}
export class HealthCacheDto {
tokenInfos: TokenInfoDto[];
serum3Infos: Serum3InfoDto[];
perpInfos: PerpInfoDto[];
}
export class TokenInfoDto {
tokenIndex: number;
maintAssetWeight: I80F48Dto;
initAssetWeight: I80F48Dto;
maintLiabWeight: I80F48Dto;
initLiabWeight: I80F48Dto;
oraclePrice: I80F48Dto; // native/native
// in health-reference-token native units
balance: I80F48Dto;
// in health-reference-token native units
serum3MaxReserved: I80F48Dto;
}
export class Serum3InfoDto {
reserved: I80F48Dto;
baseIndex: number;
quoteIndex: number;
}
export class PerpInfoDto {
maintAssetWeight: I80F48Dto;
initAssetWeight: I80F48Dto;
maintLiabWeight: I80F48Dto;
initLiabWeight: I80F48Dto;
// in health-reference-token native units, needs scaling by asset/liab
base: I80F48Dto;
// in health-reference-token native units, no asset/liab factor needed
quote: I80F48Dto;
}

View File

@ -5,6 +5,7 @@ import { MangoClient } from '../client';
import { nativeI80F48ToUi } from '../utils';
import { Bank, QUOTE_DECIMALS } from './bank';
import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
export class MangoAccount {
public tokens: TokenPosition[];
@ -318,6 +319,7 @@ export class MangoAccount {
let res = 'MangoAccount';
res = res + '\n pk: ' + this.publicKey.toString();
res = res + '\n name: ' + this.name;
res = res + '\n delegate: ' + this.delegate;
res =
this.tokensActive().length > 0
@ -505,12 +507,14 @@ export class HealthType {
export class MangoAccountData {
constructor(
public healthCache: HealthCache,
public initHealth: I80F48,
public maintHealth: I80F48,
public equity: Equity,
) {}
static from(event: {
healthCache: HealthCacheDto;
initHealth: I80F48Dto;
maintHealth: I80F48Dto;
equity: {
@ -521,6 +525,7 @@ export class MangoAccountData {
tokenAssets: any;
}) {
return new MangoAccountData(
new HealthCache(event.healthCache),
I80F48.from(event.initHealth),
I80F48.from(event.maintHealth),
Equity.from(event.equity),

View File

@ -126,6 +126,28 @@ export class PerpMarket {
const quoteUnit = Math.pow(10, QUOTE_DECIMALS);
return new BN(uiQuote * quoteUnit).div(this.quoteLotSize);
}
toString(): string {
return (
'PerpMarket ' +
'\n perpMarketIndex -' +
this.perpMarketIndex +
'\n maintAssetWeight -' +
this.maintAssetWeight.toNumber() +
'\n initAssetWeight -' +
this.initAssetWeight.toNumber() +
'\n maintLiabWeight -' +
this.maintLiabWeight.toNumber() +
'\n initLiabWeight -' +
this.initLiabWeight.toNumber() +
'\n liquidationFee -' +
this.liquidationFee.toNumber() +
'\n makerFee -' +
this.makerFee.toNumber() +
'\n takerFee -' +
this.takerFee.toNumber()
);
}
}
export class Side {

View File

@ -1,6 +1,4 @@
import { Jupiter } from '@jup-ag/core';
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
import { simulateTransaction } from '@project-serum/anchor/dist/cjs/utils/rpc';
import { getFeeRates, getFeeTier } from '@project-serum/serum';
import { Order } from '@project-serum/serum/lib/market';
import {
@ -67,24 +65,26 @@ export class MangoClient {
// Group
public async createGroup(
public async groupCreate(
groupNum: number,
testing: boolean,
insuranceMintPk: PublicKey,
): Promise<TransactionSignature> {
const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey;
return await this.program.methods
.createGroup(groupNum, testing ? 1 : 0)
.groupCreate(groupNum, testing ? 1 : 0)
.accounts({
admin: adminPk,
payer: adminPk,
insuranceMint: insuranceMintPk,
})
.rpc();
}
public async closeGroup(group: Group): Promise<TransactionSignature> {
public async groupClose(group: Group): Promise<TransactionSignature> {
const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey;
return await this.program.methods
.closeGroup()
.groupClose()
.accounts({
group: group.publicKey,
admin: adminPk,
@ -120,7 +120,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 44,
offset: 40,
},
});
}
@ -141,6 +141,7 @@ export class MangoClient {
oracleConfFilter: number,
tokenIndex: number,
name: string,
adjustmentFactor: number,
util0: number,
rate0: number,
util1: number,
@ -154,7 +155,6 @@ export class MangoClient {
initLiabWeight: number,
liquidationFee: number,
): Promise<TransactionSignature> {
const bn = I80F48.fromNumber(oracleConfFilter).getData();
return await this.program.methods
.tokenRegister(
tokenIndex,
@ -165,7 +165,7 @@ export class MangoClient {
val: I80F48.fromNumber(oracleConfFilter).getData(),
},
} as any, // future: nested custom types dont typecheck, fix if possible?
{ util0, rate0, util1, rate1, maxRate },
{ adjustmentFactor, util0, rate0, util1, rate1, maxRate },
loanFeeRate,
loanOriginationFeeRate,
maintAssetWeight,
@ -185,6 +185,61 @@ export class MangoClient {
.rpc();
}
public async tokenEdit(
group: Group,
tokenName: string,
oracle: PublicKey,
oracleConfFilter: number,
adjustmentFactor: number,
util0: number,
rate0: number,
util1: number,
rate1: number,
maxRate: number,
loanFeeRate: number,
loanOriginationFeeRate: number,
maintAssetWeight: number,
initAssetWeight: number,
maintLiabWeight: number,
initLiabWeight: number,
liquidationFee: number,
): Promise<TransactionSignature> {
const bank = group.banksMap.get(tokenName)!;
const mintInfo = group.mintInfosMap.get(bank.tokenIndex)!;
return await this.program.methods
.tokenEdit(
new BN(0),
oracle,
{
confFilter: {
val: I80F48.fromNumber(oracleConfFilter).getData(),
},
} as any, // future: nested custom types dont typecheck, fix if possible?
{ adjustmentFactor, util0, rate0, util1, rate1, maxRate },
loanFeeRate,
loanOriginationFeeRate,
maintAssetWeight,
initAssetWeight,
maintLiabWeight,
initLiabWeight,
liquidationFee,
)
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
mintInfo: mintInfo.publicKey,
})
.remainingAccounts([
{
pubkey: bank.publicKey,
isWritable: true,
isSigner: false,
} as AccountMeta,
])
.rpc({ skipPreflight: true });
}
public async tokenDeregister(
group: Group,
tokenName: string,
@ -236,7 +291,7 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
offset: 8,
},
},
])
@ -275,7 +330,7 @@ export class MangoClient {
{
memcmp: {
bytes: bs58.encode(tokenIndexBuf),
offset: 200,
offset: 40,
},
},
])
@ -286,13 +341,13 @@ export class MangoClient {
// Stub Oracle
public async createStubOracle(
public async stubOracleCreate(
group: Group,
mintPk: PublicKey,
price: number,
): Promise<TransactionSignature> {
return await this.program.methods
.createStubOracle({ val: I80F48.fromNumber(price).getData() })
.stubOracleCreate({ val: I80F48.fromNumber(price).getData() })
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
@ -302,12 +357,12 @@ export class MangoClient {
.rpc();
}
public async closeStubOracle(
public async stubOracleClose(
group: Group,
oracle: PublicKey,
): Promise<TransactionSignature> {
return await this.program.methods
.closeStubOracle()
.stubOracleClose()
.accounts({
group: group.publicKey,
oracle: oracle,
@ -317,13 +372,13 @@ export class MangoClient {
.rpc();
}
public async setStubOracle(
public async stubOracleSet(
group: Group,
oraclePk: PublicKey,
price: number,
): Promise<TransactionSignature> {
return await this.program.methods
.setStubOracle({ val: I80F48.fromNumber(price).getData() })
.stubOracleSet({ val: I80F48.fromNumber(price).getData() })
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
@ -382,7 +437,7 @@ export class MangoClient {
name?: string,
): Promise<TransactionSignature> {
return await this.program.methods
.createAccount(accountNumber, name ?? '')
.accountCreate(accountNumber, name ?? '')
.accounts({
group: group.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
@ -391,6 +446,22 @@ export class MangoClient {
.rpc();
}
public async editMangoAccount(
group: Group,
mangoAccount: MangoAccount,
name?: string,
delegate?: PublicKey,
): Promise<TransactionSignature> {
return await this.program.methods
.accountEdit(name, delegate)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.rpc();
}
public async getMangoAccount(mangoAccount: MangoAccount) {
return MangoAccount.from(
mangoAccount.publicKey,
@ -407,13 +478,13 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 40,
offset: 8,
},
},
{
memcmp: {
bytes: ownerPk.toBase58(),
offset: 72,
offset: 40,
},
},
])
@ -427,7 +498,7 @@ export class MangoClient {
mangoAccount: MangoAccount,
): Promise<TransactionSignature> {
return await this.program.methods
.closeAccount()
.accountClose()
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
@ -663,7 +734,7 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
offset: 8,
},
},
];
@ -674,7 +745,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 122,
offset: 40,
},
});
}
@ -685,7 +756,7 @@ export class MangoClient {
filters.push({
memcmp: {
bytes: bs58.encode(qbuf),
offset: 124,
offset: 42,
},
});
}
@ -991,7 +1062,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,
@ -1054,6 +1124,55 @@ export class MangoClient {
.rpc();
}
async perpEditMarket(
group: Group,
perpMarketName: string,
oracle: PublicKey,
oracleConfFilter: number,
baseTokenIndex: number,
baseTokenDecimals: number,
maintAssetWeight: number,
initAssetWeight: number,
maintLiabWeight: number,
initLiabWeight: number,
liquidationFee: number,
makerFee: number,
takerFee: number,
minFunding: number,
maxFunding: number,
impactQuantity: number,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
return await this.program.methods
.perpEditMarket(
oracle,
{
confFilter: {
val: I80F48.fromNumber(oracleConfFilter).getData(),
},
} as any, // future: nested custom types dont typecheck, fix if possible?
baseTokenIndex,
baseTokenDecimals,
maintAssetWeight,
initAssetWeight,
maintLiabWeight,
initLiabWeight,
liquidationFee,
makerFee,
takerFee,
minFunding,
maxFunding,
new BN(impactQuantity),
)
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
perpMarket: perpMarket.publicKey,
})
.rpc();
}
async perpCloseMarket(
group: Group,
perpMarketName: string,
@ -1078,7 +1197,6 @@ export class MangoClient {
public async perpGetMarkets(
group: Group,
baseTokenIndex?: number,
quoteTokenIndex?: number,
): Promise<PerpMarket[]> {
const bumpfbuf = Buffer.alloc(1);
bumpfbuf.writeUInt8(255);
@ -1087,7 +1205,7 @@ export class MangoClient {
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
offset: 8,
},
},
];
@ -1098,18 +1216,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,
},
});
}
@ -1182,14 +1289,14 @@ export class MangoClient {
inputToken,
amountIn,
outputToken,
slippage = 0.5,
userDefinedInstructions,
}: {
group: Group;
mangoAccount: MangoAccount;
inputToken: string;
amountIn: number;
outputToken: string;
slippage: number;
userDefinedInstructions: TransactionInstruction[];
}): Promise<TransactionSignature> {
const inputBank = group.banksMap.get(inputToken);
const outputBank = group.banksMap.get(outputToken);
@ -1260,8 +1367,7 @@ export class MangoClient {
}
/*
* Transfer input token to users wallet, then swap with the Jupiter route,
* and finally transfer output token from users wallet back to the mango vault
* Transfer input token to users wallet, then concat the passed in instructions
*/
const nativeInputAmount = toU64(
amountIn,
@ -1281,57 +1387,7 @@ export class MangoClient {
transferIx.keys[2] = { ...inputBankKey, isWritable: true, isSigner: false };
instructions.push(transferIx);
// TODO: move out of client and into ui
// Start Jupiter
const jupiter = await Jupiter.load({
connection: this.program.provider.connection,
cluster: 'mainnet-beta',
user: mangoAccount.owner, // or public key
// platformFeeAndAccounts: NO_PLATFORM_FEE,
routeCacheDuration: 10_000, // Will not refetch data on computeRoutes for up to 10 seconds
});
const routes = await jupiter.computeRoutes({
inputMint: inputBank.mint, // Mint address of the input token
outputMint: outputBank.mint, // Mint address of the output token
inputAmount: nativeInputAmount, // raw input amount of tokens
slippage, // The slippage in % terms
forceFetch: false, // false is the default value => will use cache if not older than routeCacheDuration
});
const routesInfosWithoutRaydium = routes.routesInfos.filter((r) => {
if (r.marketInfos.length > 1) {
for (const mkt of r.marketInfos) {
if (mkt.amm.label === 'Raydium' || mkt.amm.label === 'Serum')
return false;
}
}
return true;
});
const selectedRoute = routesInfosWithoutRaydium[0];
console.log(
`route found: ${selectedRoute.marketInfos[0].amm.label}. generating jup transaction`,
);
const { transactions } = await jupiter.exchange({
routeInfo: selectedRoute,
});
console.log('Jupiter Transactions:', transactions);
const { setupTransaction, swapTransaction } = transactions;
for (const ix of swapTransaction.instructions) {
if (
ix.programId.toBase58() ===
'JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo'
) {
instructions.push(ix);
}
}
// End Jupiter
instructions.concat(userDefinedInstructions);
const transferIx2 = Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
@ -1339,12 +1395,12 @@ export class MangoClient {
outputBank.vault,
mangoAccount.owner,
[],
selectedRoute.outAmountWithSlippage,
0, // todo: use this for testing, this should be the amount to transfer back
);
instructions.push(transferIx2);
/*
* Build data objects for margin trade instructions
* Create object of amounts that will be withdrawn from bank vaults
*/
const targetRemainingAccounts = instructions
.map((ix) => [
@ -1368,6 +1424,9 @@ export class MangoClient {
},
];
/*
* Build cpi data objects for instructions
*/
let cpiDatas = [];
for (const [index, ix] of instructions.entries()) {
if (index === 0) {
@ -1385,27 +1444,11 @@ export class MangoClient {
}
}
console.log(
'instructions',
instructions.map((i) => ({ ...i, programId: i.programId.toString() })),
);
console.log('cpiDatas', cpiDatas);
console.log(
'targetRemainingAccounts',
targetRemainingAccounts.map((t) => ({
...t,
pubkey: t.pubkey.toString(),
})),
);
if (setupTransaction) {
await this.program.provider.sendAndConfirm(setupTransaction);
} else if (preInstructions.length) {
if (preInstructions.length) {
const tx = new Transaction();
for (const ix of preInstructions) {
tx.add(ix);
}
console.log('preInstructions', preInstructions);
await this.program.provider.sendAndConfirm(tx);
}
@ -1454,6 +1497,7 @@ export class MangoClient {
isSigner: false,
} as AccountMeta),
);
console.log('1');
/*
* Find or create associated token accounts
@ -1500,6 +1544,7 @@ export class MangoClient {
),
);
}
console.log('2');
if (preInstructions.length) {
const tx = new Transaction();
@ -1510,6 +1555,7 @@ export class MangoClient {
await this.program.provider.sendAndConfirm(tx);
}
console.log('3');
const inputBankAccount = {
pubkey: inputBank.publicKey,
@ -1560,6 +1606,9 @@ export class MangoClient {
},
])
.instruction();
console.log('4');
// userDefinedInstructions.push(flashLoanEndIx);
const flashLoanBeginIx = await this.program.methods
.flashLoan3Begin([

View File

@ -3,7 +3,9 @@ import { StubOracle } from './accounts/oracle';
import { MangoClient } from './client';
import { MANGO_V4_ID } from './constants';
export * from './accounts/bank';
export * from './accounts/I80F48';
export * from './accounts/mangoAccount';
export {
Serum3Market,
Serum3OrderType,
@ -12,6 +14,4 @@ export {
} from './accounts/serum3';
export * from './constants';
export * from './utils';
export * from './accounts/mangoAccount';
export { Group, StubOracle, MangoClient, MANGO_V4_ID };

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,10 @@ import { MangoClient } from '../client';
import { MANGO_V4_ID, SERUM3_PROGRAM_ID } from '../constants';
import { Id } from '../ids';
//
// script to add a group to ids json
//
function replacer(key, value) {
if (value instanceof Map) {
return Object.fromEntries(value);

View File

@ -4,6 +4,10 @@ import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
//
export const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
]);
@ -40,7 +44,7 @@ async function main() {
const usdcDevnetOracle = (
await client.getStubOracle(group, usdcDevnetMint)
)[0];
sig = await client.closeStubOracle(group, usdcDevnetOracle.publicKey);
sig = await client.stubOracleClose(group, usdcDevnetOracle.publicKey);
console.log(
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}?cluster=devnet`,
);
@ -71,7 +75,7 @@ async function main() {
// finally, close the group
sig = await client.closeGroup(group);
sig = await client.groupClose(group);
console.log(
`Closed group, sig https://explorer.solana.com/tx/${sig}?cluster=devnet`,
);

View File

@ -4,6 +4,16 @@ import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// An example for admins based on high level api i.e. the client
// Depoys a new mango group to devnet, registers 4 tokens, and 1 serum3 spot market
//
// process.env.ADMIN_KEYPAIR - group admin keypair path
// to create a new admin keypair:
// * solana-keygen new --outfile ~/.config/solana/admin.json
// * solana airdrop 1 -k ~/.config/solana/admin.json
//
const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
@ -20,15 +30,6 @@ const DEVNET_ORACLES = new Map([
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
]);
//
// An example for admins based on high level api i.e. the client
// Depoys a new mango group to devnet, registers 2 tokens, and 1 serum3 spot market
//
// process.env.ADMIN_KEYPAIR - group admin keypair path
// to create a new admin keypair:
// * solana-keygen new --outfile ~/.config/solana/admin.json
// * solana airdrop 1 -k ~/.config/solana/admin.json
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
@ -52,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.groupCreate(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')!);
@ -70,8 +72,9 @@ async function main() {
btcDevnetMint,
btcDevnetOracle,
0.1,
0,
1, // tokenIndex
'BTC',
0.01,
0.4,
0.07,
0.8,
@ -90,11 +93,11 @@ 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 {
await client.createStubOracle(group, usdcDevnetMint, 1.0);
await client.stubOracleCreate(group, usdcDevnetMint, 1.0);
} catch (error) {
console.log(error);
}
@ -108,8 +111,9 @@ async function main() {
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
1,
0, // tokenIndex
'USDC',
0.01,
0.4,
0.07,
0.8,
@ -138,6 +142,7 @@ async function main() {
0.1,
2, // tokenIndex
'SOL',
0.01,
0.4,
0.07,
0.8,
@ -168,6 +173,7 @@ async function main() {
0.1,
3, // tokenIndex
'ORCA',
0.01,
0.4,
0.07,
0.8,
@ -227,7 +233,7 @@ async function main() {
0,
'BTC-PERP',
0.1,
0,
1,
6,
1,
10,
@ -250,10 +256,121 @@ 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}`);
//
// edit
//
console.log(`Editing USDC...`);
try {
let sig = await client.tokenEdit(
group,
'USDC',
btcDevnetOracle,
0.1,
0.01,
0.3,
0.08,
0.81,
0.91,
0.75,
0.0007,
1.7,
0.9,
0.7,
1.3,
1.5,
0.04,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.banksMap.get('USDC').toString());
} catch (error) {
throw error;
}
console.log(`Resetting USDC...`);
try {
let sig = await client.tokenEdit(
group,
'USDC',
usdcDevnetOracle.publicKey,
0.1,
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.0005,
1.5,
0.8,
0.6,
1.2,
1.4,
0.02,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.banksMap.get('USDC').toString());
} catch (error) {
throw error;
}
console.log(`Editing perp market...`);
try {
let sig = await client.perpEditMarket(
group,
'BTC-PERP',
btcDevnetOracle,
0.2,
0,
6,
0.9,
0.9,
1.035,
1.06,
0.013,
0.0003,
0.1,
0.07,
0.07,
1001,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.perpMarketsMap.get('BTC-PERP').toString());
} catch (error) {
console.log(error);
}
console.log(`Resetting perp market...`);
try {
let sig = await client.perpEditMarket(
group,
'BTC-PERP',
btcDevnetOracle,
0.1,
1,
6,
1,
0.95,
1.025,
1.05,
0.012,
0.0002,
0.0,
0.05,
0.05,
100,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.perpMarketsMap.get('BTC-PERP').toString());
} catch (error) {
console.log(error);
}
process.exit();
}

View File

@ -4,6 +4,10 @@ import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// (unfinished?) script which shows how to use the flash loan 1 ix
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(

View File

@ -3,6 +3,9 @@ import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
//
// script which shows example usage of ids json (saves having to do gpa)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(

View File

@ -5,6 +5,10 @@ import { Serum3Side } from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc.
//
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
async function main() {
const options = AnchorProvider.defaultOptions();

View File

@ -0,0 +1,167 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { OrderType, Side } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// mango account owner
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// delegate
const delegate = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(process.env.DELEGATE!, 'utf-8'))),
);
const delegateWallet = new Wallet(delegate);
const delegateProvider = new AnchorProvider(
connection,
delegateWallet,
options,
);
// Note: simply create a client with delegate and use this client to execute ixs
const delegateClient = await MangoClient.connect(
delegateProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`Delegate ${delegateWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await delegateClient.getGroupForAdmin(admin.publicKey, 0);
console.log(group.toString());
// fetch mango account using owners pubkey
console.log(`Fetching mangoaccount...`);
const mangoAccount = (
await delegateClient.getMangoAccountForOwner(group, user.publicKey)
)[0];
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
if (true) {
// set delegate, and change name
console.log(`...changing mango account name, and setting a delegate`);
await client.editMangoAccount(group, 'my_changed_name', delegate.publicKey);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
}
if (true) {
// deposit
console.log(`...depositing 50 USDC`);
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
await mangoAccount.reload(client, group);
// serum3
console.log(`...placing serum3 bid`);
await delegateClient.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
Serum3Side.bid,
20,
0.0001,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
10,
);
await mangoAccount.reload(delegateClient, group);
console.log(`...current own orders on OB`);
let orders = await delegateClient.getSerum3Orders(
group,
'BTC/USDC',
);
for (const order of orders) {
console.log(
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
);
console.log(` - cancelling order with ${order.orderId}`);
await delegateClient.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
console.log(`...settling funds`);
await delegateClient.serum3SettleFunds(
group,
mangoAccount,
'BTC/USDC',
);
}
if (true) {
// perps
console.log(`...placing perp bid`);
try {
await delegateClient.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.bid,
30000,
0.000001,
30000 * 0.000001,
Math.floor(Math.random() * 99999),
OrderType.limit,
0,
1,
);
} catch (error) {
console.log(error);
}
}
process.exit();
}
main();

View File

@ -1,6 +1,7 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { HealthType } from '../accounts/mangoAccount';
import { OrderType, Side } from '../accounts/perp';
import {
Serum3OrderType,
@ -17,6 +18,9 @@ import { toUiDecimals } from '../utils';
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
@ -58,32 +62,57 @@ async function main() {
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
await mangoAccount.reloadAccountData(client, group);
if (true) {
// deposit and withdraw
console.log(`Depositing...50 USDC`);
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
await mangoAccount.reload(client, group);
console.log(`Depositing...0.0005 BTC`);
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
await mangoAccount.reload(client, group);
console.log(`Withdrawing...0.1 ORCA`);
await client.tokenWithdraw2(
// set delegate, and change name
console.log(`...changing mango account name, and setting a delegate`);
const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
);
await client.editMangoAccount(
group,
mangoAccount,
'ORCA',
0.1 * Math.pow(10, group.banksMap.get('ORCA').mintDecimals),
true,
'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,
mangoAccount,
'my_mango_account',
PublicKey.default,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
}
if (true) {
// deposit and withdraw
console.log(`...depositing 50 USDC`);
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
await mangoAccount.reload(client, group);
// witdrawing fails if no (other) user has deposited ORCA in the group
// console.log(`Withdrawing...0.1 ORCA`);
// await client.tokenWithdraw2(
// group,
// mangoAccount,
// 'ORCA',
// 0.1 * Math.pow(10, group.banksMap.get('ORCA').mintDecimals),
// true,
// );
// await mangoAccount.reload(client, group);
// console.log(mangoAccount.toString());
// serum3
console.log(
`Placing serum3 bid which would not be settled since its relatively low then midprice...`,
`...placing serum3 bid which would not be settled since its relatively low then midprice`,
);
await client.serum3PlaceOrder(
group,
@ -100,7 +129,7 @@ async function main() {
);
await mangoAccount.reload(client, group);
console.log(`Placing serum3 bid way above midprice...`);
console.log(`...placing serum3 bid way above midprice`);
await client.serum3PlaceOrder(
group,
mangoAccount,
@ -116,7 +145,7 @@ async function main() {
);
await mangoAccount.reload(client, group);
console.log(`Placing serum3 ask way below midprice...`);
console.log(`...placing serum3 ask way below midprice`);
await client.serum3PlaceOrder(
group,
mangoAccount,
@ -131,7 +160,7 @@ async function main() {
10,
);
console.log(`Current own orders on OB...`);
console.log(`...current own orders on OB`);
let orders = await client.getSerum3Orders(
group,
@ -139,9 +168,9 @@ async function main() {
);
for (const order of orders) {
console.log(
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
);
console.log(` - Cancelling order with ${order.orderId}`);
console.log(` - cancelling order with ${order.orderId}`);
await client.serum3CancelOrder(
group,
mangoAccount,
@ -152,7 +181,7 @@ async function main() {
);
}
console.log(`Current own orders on OB...`);
console.log(`...current own orders on OB`);
orders = await client.getSerum3Orders(
group,
@ -162,7 +191,7 @@ async function main() {
console.log(order);
}
console.log(`Settling funds...`);
console.log(`...settling funds`);
await client.serum3SettleFunds(
group,
mangoAccount,
@ -174,23 +203,31 @@ async function main() {
if (true) {
await mangoAccount.reload(client, group);
console.log(
'mangoAccount.getEquity() ' +
'...mangoAccount.getEquity() ' +
toUiDecimals(mangoAccount.getEquity().toNumber()),
);
console.log(
'mangoAccount.getCollateralValue() ' +
'...mangoAccount.getCollateralValue() ' +
toUiDecimals(mangoAccount.getCollateralValue().toNumber()),
);
console.log(
'mangoAccount.getAssetsVal() ' +
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
toUiDecimals(
mangoAccount.accountData['healthCache']
.health(HealthType.init)
.toNumber(),
),
);
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimals(mangoAccount.getAssetsVal().toNumber()),
);
console.log(
'mangoAccount.getLiabsVal() ' +
'...mangoAccount.getLiabsVal() ' +
toUiDecimals(mangoAccount.getLiabsVal().toNumber()),
);
console.log(
"mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL') " +
'...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' +
toUiDecimals(
(
await mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL')
@ -198,7 +235,7 @@ async function main() {
),
);
console.log(
"mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
toUiDecimals(
mangoAccount
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
@ -206,7 +243,7 @@ async function main() {
),
);
console.log(
"mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
toUiDecimals(
mangoAccount
.getPerpMarketMarginAvailable(group, 'BTC-PERP')
@ -217,7 +254,7 @@ async function main() {
if (true) {
// perps
console.log(`Placing perp bid...`);
console.log(`...placing perp bid`);
try {
await client.perpPlaceOrder(
group,
@ -236,7 +273,7 @@ async function main() {
console.log(error);
}
console.log(`Placing perp ask...`);
console.log(`...placing perp ask`);
await client.perpPlaceOrder(
group,
mangoAccount,
@ -254,7 +291,7 @@ async function main() {
while (true) {
// TODO: quotePositionNative might be buggy on program side, investigate...
console.log(
`Waiting for self trade to consume (note: make sure keeper crank is running)...`,
`...waiting for self trade to consume (note: make sure keeper crank is running)`,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());

View File

@ -7,6 +7,10 @@ import { MangoClient } from '../client';
import { MANGO_V4_ID, SERUM3_PROGRAM_ID } from '../constants';
import { Id } from '../ids';
//
// script to add a group to ids json
//
function replacer(key, value) {
if (value instanceof Map) {
return Object.fromEntries(value);

View File

@ -4,6 +4,10 @@ import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
//
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
@ -40,7 +44,7 @@ async function main() {
const usdcMainnetBetaOracle = (
await client.getStubOracle(group, usdcMainnetBetaMint)
)[0];
sig = await client.closeStubOracle(group, usdcMainnetBetaOracle.publicKey);
sig = await client.stubOracleClose(group, usdcMainnetBetaOracle.publicKey);
console.log(
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}`,
);
@ -70,7 +74,7 @@ async function main() {
}
// finally, close the group
sig = await client.closeGroup(group);
sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
process.exit();

View File

@ -4,6 +4,10 @@ import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// Script which depoys a new mango group, and registers 3 tokens
//
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
@ -37,7 +41,7 @@ async function main() {
// group
console.log(`Creating Group...`);
try {
await client.createGroup(0, true);
await client.groupCreate(0, true);
} catch (error) {
console.log(error);
}
@ -56,6 +60,7 @@ async function main() {
0.1,
0,
'BTC',
0.01,
0.4,
0.07,
0.8,
@ -78,7 +83,7 @@ async function main() {
console.log(`Creating USDC stub oracle...`);
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
try {
await client.createStubOracle(group, usdcMainnetMint, 1.0);
await client.stubOracleCreate(group, usdcMainnetMint, 1.0);
} catch (error) {
console.log(error);
}
@ -96,6 +101,7 @@ async function main() {
0.1,
1,
'USDC',
0.01,
0.4,
0.07,
0.8,
@ -126,6 +132,7 @@ async function main() {
0.1,
2, // tokenIndex
'SOL',
0.01,
0.4,
0.07,
0.8,

View File

@ -4,6 +4,9 @@ import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
import { MangoClient } from '../client';
//
// (untested?) script which closes a mango account cleanly, first closes all positions, withdraws all tokens and then closes it
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL, options);

View File

@ -5,6 +5,10 @@ import { HealthType } from '../accounts/mangoAccount';
import { MangoClient } from '../client';
import { toUiDecimals } from '../utils';
//
// example script shows usage of ids json (saves havint to do gpa)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL, options);

View File

@ -20,6 +20,9 @@ const MANGO_MAINNET_PAYER_KEYPAIR =
process.env.MANGO_MAINNET_PAYER_KEYPAIR ||
'/Users/tylershipe/.config/solana/deploy.json';
//
// example script which shows usage of flash loan 3 ix using a jupiter swap
//
// NOTE: we assume that ATA for source and target already exist for wallet
async function main() {
const options = AnchorProvider.defaultOptions();

View File

@ -110,7 +110,7 @@ async function main() {
'devnet',
MANGO_V4_ID['devnet'],
);
await client.setStubOracle(group, group.banksMap.get('USDC')?.oracle!, 0.5);
await client.stubOracleSet(group, group.banksMap.get('USDC')?.oracle!, 0.5);
process.exit();
}

9
tsconfig.esm.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.build",
"compilerOptions": {
"declaration": true,
"declarationDir": "dist/types",
"module": "esnext",
"outDir": "dist/esm"
}
}

View File

@ -10,6 +10,11 @@
"skipLibCheck": true,
"target": "es2019"
},
"include": ["ts/client/src"],
"exclude": ["./ts/**/*.test.js", "node_modules", "**/node_modules"]
"include": ["ts/client/src", "ts/client/scripts", "ts/client/scripts"],
"exclude": [
"./ts/**/*.test.js",
"node_modules",
"**/node_modules",
"./ts/client/src/scripts"
]
}

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)

View File

@ -111,16 +111,16 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@jup-ag/core@^1.0.0-beta.27":
version "1.0.0-beta.27"
resolved "https://registry.yarnpkg.com/@jup-ag/core/-/core-1.0.0-beta.27.tgz#e5d8ca75ef827b5cb809384c139c371b579a29c5"
integrity sha512-AIGhvDkh16bfagE3IT/DDORAM7Fak+RWPMoHSdCfRE5tqFjyqnJM8c+zbEh3+lhOWIs7iEMi/buiGKamNzx9fA==
"@jup-ag/core@^1.0.0-beta.28":
version "1.0.0-beta.28"
resolved "https://registry.yarnpkg.com/@jup-ag/core/-/core-1.0.0-beta.28.tgz#0c15f940c6049f8b8570d6667d716f1bb1010434"
integrity sha512-96gb77z34jAn0WSQDqdrS8qQdCBRVFFv4UP5/N0d+oyPbK0jpIpAL7TIGN8+RCuutJB3nLLAmiNNVWYf+aJaFg==
dependencies:
"@jup-ag/crema-sdk" "2.0.7"
"@jup-ag/cykura-sdk" "0.1.25"
"@jup-ag/cykura-sdk-core" "0.1.8"
"@jup-ag/lifinity-sdk" "0.1.72"
"@jup-ag/math" "1.0.0-beta.27"
"@jup-ag/math" "1.0.0-beta.28"
"@jup-ag/whirlpool-sdk" "0.1.1"
"@mercurial-finance/optimist" "0.1.4"
"@project-serum/anchor" "0.23.0"
@ -173,10 +173,10 @@
"@solana/web3.js" "1.31.0"
decimal.js "^10.3.1"
"@jup-ag/math@1.0.0-beta.27":
version "1.0.0-beta.27"
resolved "https://registry.yarnpkg.com/@jup-ag/math/-/math-1.0.0-beta.27.tgz#5d3f0f822586ee86b2c46bce392d3801bb3fda9b"
integrity sha512-jJaMRYjVG0oo9js/BkILVNPkBHnzUW9sQDtu9pH/JXis+VPz99Ud+wZV3mXYEo/kHinvJTsb5QuPAhtM32w+TA==
"@jup-ag/math@1.0.0-beta.28":
version "1.0.0-beta.28"
resolved "https://registry.yarnpkg.com/@jup-ag/math/-/math-1.0.0-beta.28.tgz#52300d7470eee7c2cdac01b27b2f0c16062ad48a"
integrity sha512-GQkziJQ5kJwYgYNvK8ZFpW+d92R90ihwiQ2n1BunKakeTvPdwt7BQKAOj4KRgIb9/458zMCuYVZvdF1cAWFA/g==
dependencies:
decimal.js "10.3.1"
jsbi "4.3.0"
@ -367,19 +367,19 @@
"@solana/web3.js" "^1.30.2"
buffer "^6.0.1"
"@saberhq/option-utils@^1.13.30":
version "1.13.30"
resolved "https://registry.yarnpkg.com/@saberhq/option-utils/-/option-utils-1.13.30.tgz#96d76dc87fc137bc7ae84cf515fbd758df88734d"
integrity sha512-/YtbfR1pUqHD9umByNdM5Qf5u2p2TcAa4RKpq2dnBhf56bk/N1e2n2h0UJ+cZXKH6WAqTaN/cuoh13QTtE1Y5w==
"@saberhq/option-utils@^1.13.32":
version "1.13.32"
resolved "https://registry.yarnpkg.com/@saberhq/option-utils/-/option-utils-1.13.32.tgz#143eb446c2c8d743690a900ae1688c9209fbcd05"
integrity sha512-xkpXZ82EGPJwT+CGjyhZ6mAQW0KpY+3hBaXAhrkv6M0IPgj2xGRYJ+WdwBvF/WeTgmFxGHCp7o1DmwjR2/XXbA==
dependencies:
tslib "^2.4.0"
"@saberhq/solana-contrib@^1.12.66", "@saberhq/solana-contrib@^1.13.30", "@saberhq/solana-contrib@^1.13.6":
version "1.13.30"
resolved "https://registry.yarnpkg.com/@saberhq/solana-contrib/-/solana-contrib-1.13.30.tgz#3b35ab351f33522bcae0ae3b3e231fec07dbfd35"
integrity sha512-4itF/Dw8O3AP/51vDoxExEF8WD7xcnXGKGWo6D3ITQlrLPZd3PboZHQVuhByoz0VB2j9hZeP3sLCtN9vh9Nd7w==
"@saberhq/solana-contrib@^1.12.66", "@saberhq/solana-contrib@^1.13.32", "@saberhq/solana-contrib@^1.13.6":
version "1.13.32"
resolved "https://registry.yarnpkg.com/@saberhq/solana-contrib/-/solana-contrib-1.13.32.tgz#ace608df953c1e92b8cad388835dce5d1729f7a7"
integrity sha512-W0F5W1CJjk2ACuAYjGxr/nB+pDHsmg2A6F+d+XRM6/EES/9ZoIINHCA1dnPsTa0p4PnC1S7BtCrv9aERzwRKhg==
dependencies:
"@saberhq/option-utils" "^1.13.30"
"@saberhq/option-utils" "^1.13.32"
"@solana/buffer-layout" "^4.0.0"
"@types/promise-retry" "^1.1.3"
"@types/retry" "^0.12.2"
@ -401,11 +401,11 @@
tslib "^2.4.0"
"@saberhq/token-utils@^1.12.66", "@saberhq/token-utils@^1.13.6":
version "1.13.30"
resolved "https://registry.yarnpkg.com/@saberhq/token-utils/-/token-utils-1.13.30.tgz#9f3ad35f21ddc6ee6f2b77fd6719cf2aec72af96"
integrity sha512-ZpzYJ/tkWmgEnIZGTPONtmKuoJjT+mpzi/8jIl2xoQjeg37S3/iuthQO/Pgw2O9UK/4j249tqqG0V/NFvPMM5w==
version "1.13.32"
resolved "https://registry.yarnpkg.com/@saberhq/token-utils/-/token-utils-1.13.32.tgz#2acc98bd4d3732b826396a70b958198e0d20dee8"
integrity sha512-n5ECiw82IQJwyq9bTkcrbNWVi+lAQoQlJlTmIye8odUQATBsqOWN+clqfrFkn/UMmezO60bo34bUaM0Oir7Pew==
dependencies:
"@saberhq/solana-contrib" "^1.13.30"
"@saberhq/solana-contrib" "^1.13.32"
"@solana/buffer-layout" "^4.0.0"
"@solana/spl-token" "^0.1.8"
"@ubeswap/token-math" "^5.1.6"
@ -540,9 +540,9 @@
tweetnacl "^1.0.0"
"@solana/web3.js@^1.32.0":
version "1.44.2"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.44.2.tgz#5303efd94a7f2d6054a1483a4b4db4a26eb2a392"
integrity sha512-DvrJMoKonLuaX0/KyyJXcP/+w+9q8mve4gN3hC2Ptg51K/Gi1/cx6oQN2lbRZb4wYPBd2s2GDAJAJUAwZGsEug==
version "1.47.3"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.47.3.tgz#ec13f2cf4f9f54cc4fbd26d20be1e026c6e2279c"
integrity sha512-TQJulaN/+b0xXq5EhQAYFwVyOORxSyVJn1EiXupClZm8DY7f9EeUG6vl0FzSAgwEAwXKsgK3sVs/3px2e7H7dQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@ethersproject/sha2" "^5.5.0"
@ -556,7 +556,7 @@
jayson "^3.4.4"
js-sha3 "^0.8.0"
node-fetch "2"
rpc-websockets "^7.4.2"
rpc-websockets "^7.5.0"
secp256k1 "^4.0.2"
superstruct "^0.14.2"
tweetnacl "^1.0.0"
@ -567,9 +567,9 @@
integrity sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==
"@types/big.js@^6.1.3":
version "6.1.3"
resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.3.tgz#c008dec4dae24c7a338ebb4521c46e9609020807"
integrity sha512-fHh2h1cFlvGP0kFCqoAsnuQoM0n3xHB6HxgZvELt7dji+BtK/j938MRL0nG5AA45EgibuFcPjgLlkqfUPCyoKw==
version "6.1.5"
resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.5.tgz#ff06b43a4c0c4002522e0fd7fc28bb963722ab01"
integrity sha512-UiWyJ6TLWoHeHZ8VUyngzCOwJDVxTsPnqfAMR/85X93rkRk5A4T2U42BCx0wCmZdtMHGHN/utJ8ft5xWu0V1bA==
"@types/bn.js@^4.11.5":
version "4.11.6"
@ -1981,9 +1981,9 @@ lru-cache@^6.0.0:
yallist "^4.0.0"
lru-cache@^7.9.0:
version "7.10.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.10.1.tgz#db577f42a94c168f676b638d15da8fb073448cab"
integrity sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==
version "7.12.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.12.0.tgz#be2649a992c8a9116efda5c487538dcf715f3476"
integrity sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw==
lunr@^2.3.9:
version "2.3.9"
@ -2354,6 +2354,19 @@ rpc-websockets@^7.4.2:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
rpc-websockets@^7.5.0:
version "7.5.0"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748"
integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==
dependencies:
"@babel/runtime" "^7.17.2"
eventemitter3 "^4.0.7"
uuid "^8.3.2"
ws "^8.5.0"
optionalDependencies:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"