Merge branch 'dev'

This commit is contained in:
microwavedcola1 2022-09-01 09:52:29 +02:00
commit 5ff181d01d
81 changed files with 4804 additions and 2072 deletions

View File

@ -11,6 +11,11 @@ on:
description: 'Docker Image Name'
required: true
type: string
imageTag:
description: 'Docker Image Tag'
required: true
type: string
default: 'latest'
jobs:
deploy:
@ -27,7 +32,7 @@ jobs:
- name: Push
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
run: heroku container:push ${{ inputs.imageName }} -a ${{ inputs.appName }} --recursive
run: heroku container:push ${{ inputs.imageName }} -a ${{ inputs.appName }} --recursive --arg BASE_TAG=${{ inputs.imageTag }}
- name: Release
env:

View File

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v2
with:
submodules: recursive
# Use docker buildx
- name: Use docker buildx
uses: docker/setup-buildx-action@v2
@ -47,8 +47,8 @@ jobs:
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}
# Build and push the image, leveraging layer caching
- name: Build and Push
# Build and push the base image, leveraging layer caching
- name: Build and Push Base Image
uses: docker/build-push-action@v2
with:
context: .
@ -58,3 +58,23 @@ jobs:
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:latest
cache-from: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache
cache-to: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache,mode=max
# Build and push the liquidator runtime image
- name: Build and Push Liquidator
uses: docker/build-push-action@v2
with:
file: liquidator/Dockerfile.liquidator
context: .
push: true
tags: |
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:${{ github.sha }}
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:latest
# Build and push the keeper runtime image
- name: Build and Push Keeper
uses: docker/build-push-action@v2
with:
file: keeper/Dockerfile.keeper
context: .
push: true
tags: |
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:${{ github.sha }}
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:latest

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ ts/client/**/*.js
ts/client/**/*.js.map
migrations/*.js
migrations/*.js.map
ts/client/src/scripts/archive/ts.ts

View File

@ -4,6 +4,9 @@ Update this for each mainnet deployment.
## not on mainnet
- Add HealthRegionBegin, -End instructions
- Add explicit "oracle" account argument for TokenDeposit and TokenWithdraw instructions
## mainnet
Aug 20, 2022 at 19:58:29 Central European Summer Time

1
Cargo.lock generated
View File

@ -3208,6 +3208,7 @@ dependencies = [
"fixed",
"fixed-macro",
"itertools 0.10.3",
"lazy_static",
"log 0.4.17",
"mango-macro",
"margin-trade",

View File

@ -9,12 +9,12 @@ COPY ./ .
RUN sed -i 's|lib/\*|lib/checked_math|' Cargo.toml
# Mount cache for downloaded and compiled dependencies
RUN --mount=type=cache,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
--mount=type=cache,target=target \
RUN --mount=type=cache,mode=0777,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
--mount=type=cache,mode=0777,target=target \
cargo build --release --bins
# Copy bins out of cache
RUN --mount=type=cache,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/
RUN --mount=type=cache,mode=0777,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/
FROM debian:bullseye-slim as run
RUN apt-get update && apt-get -y install ca-certificates libc6

0
Program Normal file
View File

View File

@ -203,7 +203,7 @@ impl ChainData {
.iter()
.rev()
.find(|w| self.is_account_write_live(w))?;
Some((pubkey.clone(), latest_good_write.clone()))
Some((*pubkey, latest_good_write.clone()))
})
.collect()
}

View File

@ -28,11 +28,10 @@ impl AccountFetcher {
&self,
address: &Pubkey,
) -> anyhow::Result<T> {
Ok(self
Ok(*self
.fetch_raw(address)?
.load::<T>()
.with_context(|| format!("loading account {}", address))?
.clone())
.with_context(|| format!("loading account {}", address))?)
}
pub fn fetch_mango_account(&self, address: &Pubkey) -> anyhow::Result<MangoAccountValue> {
@ -40,12 +39,12 @@ impl AccountFetcher {
let data = acc.data();
let disc_bytes = &data[0..8];
if disc_bytes != &MangoAccount::discriminator() {
if disc_bytes != MangoAccount::discriminator() {
anyhow::bail!("not a mango account at {}", address);
}
Ok(MangoAccountValue::from_bytes(&data[8..])
.with_context(|| format!("loading mango account {}", address))?)
MangoAccountValue::from_bytes(&data[8..])
.with_context(|| format!("loading mango account {}", address))
}
// fetches via RPC, stores in ChainData, returns new version
@ -73,7 +72,7 @@ impl AccountFetcher {
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
let response = self
.rpc
.get_account_with_commitment(&address, self.rpc.commitment())
.get_account_with_commitment(address, self.rpc.commitment())
.with_context(|| format!("refresh account {} via rpc", address))?;
let slot = response.context.slot;
let account = response

View File

@ -374,6 +374,7 @@ impl MangoClient {
account: self.mango_account_address,
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
oracle: mint_info.oracle,
token_account: get_associated_token_address(
&self.owner(),
&mint_info.mint,
@ -425,6 +426,7 @@ impl MangoClient {
owner: self.owner(),
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
oracle: mint_info.oracle,
token_account: get_associated_token_address(
&self.owner(),
&mint_info.mint,
@ -603,6 +605,10 @@ impl MangoClient {
let rates = get_fee_rates(fee_tier);
(s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty)
};
let payer_mint_info = match side {
Serum3Side::Bid => s3.quote.mint_info,
Serum3Side::Ask => s3.base.mint_info,
};
self.program()
.request()
@ -614,10 +620,8 @@ impl MangoClient {
group: self.group(),
account: self.mango_account_address,
open_orders,
quote_bank: s3.quote.mint_info.first_bank(),
quote_vault: s3.quote.mint_info.first_vault(),
base_bank: s3.base.mint_info.first_bank(),
base_vault: s3.base.mint_info.first_vault(),
payer_bank: payer_mint_info.first_bank(),
payer_vault: payer_mint_info.first_vault(),
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,

View File

@ -217,10 +217,9 @@ impl MangoGroupContext {
oracles.push(mint_info.oracle);
}
for affected_token_index in affected_tokens {
if account
if !account
.active_token_positions()
.find(|p| p.token_index == affected_token_index)
.is_none()
.any(|p| p.token_index == affected_token_index)
{
// If there is not yet an active position for the token, we need to pass
// the bank/oracle for health check anyway.

View File

@ -1,6 +1,7 @@
# Dockerfile for keeper service in Heroku
# heroku container:push keeper -R -a HEROKU_APP_NAME
# heroku container:release -a HEROKU_APP_NAME
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:latest
ARG BASE_TAG=latest
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
ENTRYPOINT ["keeper"]
CMD ["crank"]

View File

@ -1,4 +1,4 @@
use std::{sync::Arc, time::Duration};
use std::{sync::Arc, time::Duration, time::Instant};
use crate::MangoClient;
use itertools::Itertools;
@ -76,7 +76,7 @@ pub async fn loop_update_index_and_rate(
let token_names = token_indices_clone
.iter()
.map(|token_index| client.context.token(*token_index).name.to_owned())
.join(", ");
.join(",");
let program = client.program();
let mut req = program.request();
@ -112,16 +112,24 @@ pub async fn loop_update_index_and_rate(
ix.accounts.append(&mut banks);
req = req.instruction(ix);
}
let pre = Instant::now();
let sig_result = req.send().map_err(prettify_client_error);
if let Err(e) = sig_result {
log::info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names,
pre.elapsed().as_millis(),
e
);
log::error!("{:?}", e)
} else {
log::info!(
"update_index_and_rate {} {:?}",
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names,
sig_result.unwrap()
)
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
Ok(())
@ -191,6 +199,7 @@ pub async fn loop_consume_events(
event_queue.pop_front()?;
}
let pre = Instant::now();
let sig_result = client
.program()
.request()
@ -216,13 +225,22 @@ pub async fn loop_consume_events(
.map_err(prettify_client_error);
if let Err(e) = sig_result {
log::info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
ams_.len(),
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"consume_event {} {:?}",
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
sig_result.unwrap()
)
pre.elapsed().as_millis(),
ams_.len(),
);
log::info!("{:?}", sig_result);
}
Ok(())
@ -253,6 +271,7 @@ pub async fn loop_update_funding(
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let pre = Instant::now();
let sig_result = client
.program()
.request()
@ -275,13 +294,20 @@ pub async fn loop_update_funding(
.send()
.map_err(prettify_client_error);
if let Err(e) = sig_result {
log::error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"update_funding {} {:?}",
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name(),
sig_result.unwrap()
)
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
Ok(())

View File

@ -27,7 +27,7 @@ pub async fn runner(
market_name
.split('/')
.collect::<Vec<&str>>()
.get(0)
.first()
.unwrap(),
)
.unwrap();
@ -77,7 +77,7 @@ fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let account = mango_client.mango_account()?;
for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() {
if account.serum3_orders(*market_index).is_none() {
if account.serum3_orders(*market_index).is_err() {
mango_client.serum3_create_open_orders(serum3_market.market.name())?;
}
}

View File

@ -1,5 +1,6 @@
# Dockerfile for keeper service in Heroku
# heroku container:push keeper -R -a HEROKU_APP_NAME
# heroku container:release -a HEROKU_APP_NAME
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:latest
ARG BASE_TAG=latest
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
CMD ["liquidator"]

View File

@ -4,8 +4,8 @@ use crate::account_shared_data::KeyedAccountSharedData;
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
use mango_v4::state::{
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
MangoAccountValue, TokenIndex, QUOTE_TOKEN_INDEX,
new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue,
TokenIndex, QUOTE_TOKEN_INDEX,
};
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
@ -143,11 +143,10 @@ pub fn maybe_liquidate_account(
let token = mango_client.context.token(token_position.token_index);
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
let price = oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
bank.oracle_config.conf_filter,
bank.mint_decimals,
)?;
let price = bank.oracle_price(&KeyedAccountSharedData::new(
token.mint_info.oracle,
oracle.into(),
))?;
Ok((
token_position.token_index,
price,

View File

@ -332,13 +332,13 @@ fn liquidate<'a>(
config: &liquidate::Config,
rebalance_config: &rebalance::Config,
) -> anyhow::Result<()> {
if !liquidate::maybe_liquidate_one(&mango_client, &account_fetcher, accounts, &config) {
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) {
return Ok(());
}
let liqor = &mango_client.mango_account_address;
if let Err(err) =
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, &rebalance_config)
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config)
{
log::error!("failed to rebalance liqor: {:?}", err);
}

View File

@ -1,7 +1,7 @@
use crate::{account_shared_data::KeyedAccountSharedData, AnyhowWrap};
use client::{chain_data, AccountFetcher, MangoClient, TokenContext};
use mango_v4::state::{oracle_price, Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
@ -44,11 +44,10 @@ impl TokenState {
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<I80F48> {
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
bank.oracle_config.conf_filter,
bank.mint_decimals,
)
bank.oracle_price(&KeyedAccountSharedData::new(
token.mint_info.oracle,
oracle.into(),
))
.map_err_anyhow()
}
}

View File

@ -58,3 +58,4 @@ async-trait = "0.1.52"
margin-trade = { path = "../margin-trade", features = ["cpi"] }
itertools = "0.10.3"
rand = "0.8.4"
lazy_static = "1.4.0"

View File

@ -19,6 +19,8 @@ pub enum MangoError {
InvalidFlashLoanTargetCpiProgram,
#[msg("health must be positive")]
HealthMustBePositive,
#[msg("health must be positive or increase")]
HealthMustBePositiveOrIncrease,
#[msg("health must be negative")]
HealthMustBeNegative,
#[msg("the account is bankrupt")]
@ -35,6 +37,8 @@ pub enum MangoError {
Serum3OpenOrdersExistAlready,
#[msg("bank vault has insufficent funds")]
InsufficentBankVaultFunds,
#[msg("account is currently being liquidated")]
BeingLiquidated,
}
pub trait Contextable {

View File

@ -4,8 +4,8 @@ use crate::group_seeds;
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
use crate::state::MangoAccount;
use crate::state::{
compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever,
AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, TokenIndex,
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, AccountRetriever,
Bank, Group, TokenIndex,
};
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
@ -72,7 +72,7 @@ pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
let token_accounts = &ctx.remaining_accounts[2 * num_loans..3 * num_loans];
let group_ai = &ctx.remaining_accounts[3 * num_loans];
let group_al = AccountLoader::<Group>::try_from(&group_ai)?;
let group_al = AccountLoader::<Group>::try_from(group_ai)?;
let group = group_al.load()?;
let group_seeds = group_seeds!(group);
let seeds = [&group_seeds[..]];
@ -316,7 +316,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
// Check health before balance adjustments
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let _pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
let pre_health = account.check_health_pre(&health_cache)?;
// Prices for logging
let mut prices = vec![];
@ -388,13 +389,9 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
});
// Check health after account position changes
let post_health =
compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?;
msg!("post_cpi_health {:?}", post_health);
require!(post_health >= 0, MangoError::HealthMustBePositive);
account
.fixed
.maybe_recover_from_being_liquidated(post_health);
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
account.check_health_post(&health_cache, pre_health)?;
// Deactivate inactive token accounts after health check
for raw_token_index in deactivated_token_positions {

View File

@ -0,0 +1,110 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use anchor_lang::Discriminator;
use fixed::types::I80F48;
/// Sets up for a health region
///
/// The same transaction must have the corresponding HealthRegionEnd call.
///
/// remaining_accounts: health accounts for account
#[derive(Accounts)]
pub struct HealthRegionBegin<'info> {
/// Instructions Sysvar for instruction introspection
/// CHECK: fixed instructions sysvar account
#[account(address = tx_instructions::ID)]
pub instructions: UncheckedAccount<'info>,
#[account(mut)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
}
/// Ends a health region.
///
/// remaining_accounts: health accounts for account
#[derive(Accounts)]
pub struct HealthRegionEnd<'info> {
#[account(mut)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
}
pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionBegin<'info>>,
) -> Result<()> {
// Check if the other instructions in the transactions are compatible
{
let ixs = ctx.accounts.instructions.as_ref();
let current_index = tx_instructions::load_current_index_checked(ixs)? as usize;
// There must be a matching HealthRegionEnd instruction
let mut index = current_index + 1;
let mut found_end = false;
loop {
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
Ok(ix) => ix,
Err(ProgramError::InvalidArgument) => break, // past the last instruction
Err(e) => return Err(e.into()),
};
index += 1;
if ix.program_id != crate::id() {
continue;
}
if ix.data[0..8] != crate::instruction::HealthRegionEnd::discriminator() {
continue;
}
// check that it's for the same account
require_keys_eq!(ix.accounts[0].pubkey, ctx.accounts.account.key());
found_end = true;
index += 1;
}
require_msg!(
found_end,
"found no HealthRegionEnd instruction in transaction"
);
}
let mut account = ctx.accounts.account.load_mut()?;
require_msg!(
!account.fixed.is_in_health_region(),
"account must not already be health wrapped"
);
account.fixed.set_in_health_region(true);
let group = account.fixed.group;
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
.context("create account retriever")?;
// Compute pre-health and store it on the account
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
let pre_health = account.check_health_pre(&health_cache)?;
account.fixed.health_region_begin_init_health = pre_health.ceil().checked_to_num().unwrap();
Ok(())
}
pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>,
) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
require_msg!(
account.fixed.is_in_health_region(),
"account must be health wrapped"
);
account.fixed.set_in_health_region(false);
let group = account.fixed.group;
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
.context("create account retriever")?;
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
let pre_health = I80F48::from(account.fixed.health_region_begin_init_health);
account.check_health_post(&health_cache, pre_health)?;
account.fixed.health_region_begin_init_health = 0;
Ok(())
}

View File

@ -89,6 +89,7 @@ pub fn liq_token_bankruptcy(
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
@ -106,7 +107,7 @@ pub fn liq_token_bankruptcy(
let mut liab_deposit_index = liab_bank.deposit_index;
let liab_borrow_index = liab_bank.borrow_index;
let (liqee_liab, liqee_raw_token_index) = liqee.token_position_mut(liab_token_index)?;
let initial_liab_native = liqee_liab.native(&liab_bank);
let initial_liab_native = liqee_liab.native(liab_bank);
let mut remaining_liab_loss = -initial_liab_native;
require_gt!(remaining_liab_loss, I80F48::ZERO);
@ -178,9 +179,11 @@ pub fn liq_token_bankruptcy(
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?;
// Check liqor's health
let liqor_health =
compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
if !liqor.fixed.is_in_health_region() {
let liqor_health =
compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
// liqor quote
emit!(TokenBalanceLog {
@ -304,7 +307,7 @@ pub fn liq_token_bankruptcy(
mango_group: ctx.accounts.group.key(),
liqee: ctx.accounts.liqee.key(),
liqor: ctx.accounts.liqor.key(),
liab_token_index: liab_token_index,
liab_token_index,
initial_liab_native: initial_liab_native.to_bits(),
liab_price: liab_price.to_bits(),
insurance_token_index: QUOTE_TOKEN_INDEX,

View File

@ -50,6 +50,7 @@ pub fn liq_token_with_token(
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
let mut liqee = ctx.accounts.liqee.load_mut()?;
@ -62,8 +63,7 @@ pub fn liq_token_with_token(
// we want to allow liquidation to continue until init_health is positive,
// to prevent constant oscillation between the two states
if liqee.being_liquidated() {
if init_health > I80F48::ZERO {
liqee.fixed.set_being_liquidated(false);
if liqee.fixed.maybe_recover_from_being_liquidated(init_health) {
msg!("Liqee init_health above zero");
return Ok(());
}
@ -150,7 +150,7 @@ pub fn liq_token_with_token(
let (liqor_liab_active, loan_origination_fee) =
liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(&liab_bank);
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
@ -161,7 +161,7 @@ pub fn liq_token_with_token(
let liqee_asset_active =
asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?;
let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(&asset_bank);
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
// Update the health cache
liqee_health_cache.adjust_token_balance(
@ -251,9 +251,11 @@ pub fn liq_token_with_token(
.maybe_recover_from_being_liquidated(liqee_init_health);
// Check liqor's health
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
if !liqor.fixed.is_in_health_region() {
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
emit!(LiquidateTokenAndTokenLog {
mango_group: ctx.accounts.group.key(),

View File

@ -8,6 +8,7 @@ pub use flash_loan::*;
pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use health_region::*;
pub use liq_token_bankruptcy::*;
pub use liq_token_with_token::*;
pub use perp_cancel_all_orders::*;
@ -51,6 +52,7 @@ mod flash_loan;
mod group_close;
mod group_create;
mod group_edit;
mod health_region;
mod liq_token_bankruptcy;
mod liq_token_with_token;
mod perp_cancel_all_orders;

View File

@ -4,8 +4,8 @@ use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::MangoAccount;
use crate::state::{
compute_health, new_fixed_order_account_retriever, oracle_price, AccountLoaderDynamic, Book,
BookSide, EventQueue, Group, HealthType, OrderType, PerpMarket, Side,
new_fixed_order_account_retriever, new_health_cache, oracle_price, AccountLoaderDynamic, Book,
BookSide, EventQueue, Group, OrderType, PerpMarket, Side,
};
#[derive(Accounts)]
@ -83,60 +83,81 @@ pub fn perp_place_order(
let account_pk = ctx.accounts.account.key();
{
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let perp_market_index = {
let perp_market = ctx.accounts.perp_market.load()?;
perp_market.perp_market_index
};
let (_, perp_position_raw_index) = account.ensure_perp_position(perp_market_index)?;
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
//
// Pre-health computation, _after_ perp position is created
//
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
let pre_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_health))
} else {
None
};
let oracle_price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
perp_market.oracle_config.conf_filter,
perp_market.base_token_decimals,
)?;
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let mut book = Book::new(bids, asks);
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
let mut event_queue = ctx.accounts.event_queue.load_mut()?;
// TODO reduce_only based on event queue
let oracle_price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
perp_market.oracle_config.conf_filter,
perp_market.base_token_decimals,
)?;
book.new_order(
side,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
)?;
let now_ts = Clock::get()?.unix_timestamp as u64;
let time_in_force = if expiry_timestamp != 0 {
// If expiry is far in the future, clamp to 255 seconds
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
if tif == 0 {
// If expiry is in the past, ignore the order
msg!("Order is already expired");
return Ok(());
}
tif as u8
} else {
// Never expire
0
};
// TODO reduce_only based on event queue
book.new_order(
side,
&mut perp_market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&account_pk,
price_lots,
max_base_lots,
max_quote_lots,
order_type,
time_in_force,
client_order_id,
now_ts,
limit,
)?;
//
// Health check
//
if let Some((mut health_cache, pre_health)) = pre_health_opt {
let perp_position = account.perp_position_by_raw_index(perp_position_raw_index);
health_cache.recompute_perp_info(perp_position, &perp_market)?;
account.check_health_post(&health_cache, pre_health)?;
}
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);
account.fixed.maybe_recover_from_being_liquidated(health);
Ok(())
}

View File

@ -7,12 +7,15 @@ use crate::state::*;
pub struct Serum3CancelAllOrders<'info> {
pub group: AccountLoader<'info, Group>,
#[account(has_one = group)]
#[account(
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(
@ -46,6 +49,7 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
//
{
let account = ctx.accounts.account.load()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
@ -53,11 +57,10 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
let serum_market = ctx.accounts.serum_market.load()?;
// Validate open_orders
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)
.ok_or_else(|| error!(MangoError::SomeError))?
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError

View File

@ -3,23 +3,24 @@ use anchor_lang::prelude::*;
use serum_dex::instruction::CancelOrderInstructionV2;
use crate::error::*;
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
use super::OpenOrdersSlim;
use super::Serum3Side;
use checked_math as cm;
#[derive(Accounts)]
pub struct Serum3CancelOrder<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(
@ -59,16 +60,16 @@ pub fn serum3_cancel_order(
//
{
let account = ctx.accounts.account.load()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
);
// Validate open_orders
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)
.ok_or_else(|| error!(MangoError::SomeError))?
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
@ -78,55 +79,15 @@ pub fn serum3_cancel_order(
//
// Cancel
//
let before_oo = {
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
OpenOrdersSlim::from_oo(&open_orders)
};
let order = serum_dex::instruction::CancelOrderInstructionV2 {
side: u8::try_from(side).unwrap().try_into().unwrap(),
order_id,
};
cpi_cancel_order(ctx.accounts, order)?;
{
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
let mut account = ctx.accounts.account.load_mut()?;
decrease_maybe_loan(
serum_market.market_index,
&mut account.borrow_mut(),
&before_oo,
&after_oo,
);
};
Ok(())
}
// if free has increased, the free increase is reduction in reserved, reduce this from
// the cached
pub fn decrease_maybe_loan(
market_index: Serum3MarketIndex,
account: &mut MangoAccountRefMut,
before_oo: &OpenOrdersSlim,
after_oo: &OpenOrdersSlim,
) {
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
if after_oo.native_coin_free > before_oo.native_coin_free {
let native_coin_free_increase = after_oo.native_coin_free - before_oo.native_coin_free;
serum3_account.previous_native_coin_reserved =
cm!(serum3_account.previous_native_coin_reserved - native_coin_free_increase);
}
// pc
if after_oo.native_pc_free > before_oo.native_pc_free {
let free_pc_increase = after_oo.native_pc_free - before_oo.native_pc_free;
serum3_account.previous_native_pc_reserved =
cm!(serum3_account.previous_native_pc_reserved - free_pc_increase);
}
}
fn cpi_cancel_order(ctx: &Serum3CancelOrder, order: CancelOrderInstructionV2) -> Result<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;

View File

@ -7,7 +7,11 @@ use crate::state::*;
pub struct Serum3CloseOpenOrders<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -23,7 +27,7 @@ pub struct Serum3CloseOpenOrders<'info> {
pub serum_market_external: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(mut)]
@ -36,6 +40,7 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
// Validation
//
let mut account = ctx.accounts.account.load_mut()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
@ -43,11 +48,10 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
let serum_market = ctx.accounts.serum_market.load()?;
// Validate open_orders
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)
.ok_or_else(|| error!(MangoError::SomeError))?
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
@ -58,7 +62,14 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
//
cpi_close_open_orders(ctx.accounts)?;
// TODO: decrement in_use_count on the base token and quote token
// Reduce the in_use_count on the token positions - they no longer need to be forced open.
// We cannot immediately dust tiny positions because we don't have the banks.
let (base_position, _) = account.token_position_mut(serum_market.base_token_index)?;
base_position.in_use_count = base_position.in_use_count.saturating_sub(1);
let (quote_position, _) = account.token_position_mut(serum_market.quote_token_index)?;
quote_position.in_use_count = quote_position.in_use_count.saturating_sub(1);
// Deactivate the serum open orders account itself
account.deactivate_serum3_orders(serum_market.market_index)?;
Ok(())

View File

@ -7,7 +7,11 @@ use crate::state::*;
pub struct Serum3CreateOpenOrders<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
@ -48,6 +52,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()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError

View File

@ -1,12 +1,14 @@
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::instructions::{
apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersSlim,
};
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
#[derive(Accounts)]
pub struct Serum3LiqForceCancelOrders<'info> {
pub group: AccountLoader<'info, Group>,
@ -15,7 +17,7 @@ pub struct Serum3LiqForceCancelOrders<'info> {
pub account: AccountLoaderDynamic<'info, MangoAccount>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(
@ -50,7 +52,7 @@ pub struct Serum3LiqForceCancelOrders<'info> {
/// CHECK: Validated by the serum cpi call
pub market_vault_signer: UncheckedAccount<'info>,
// token_index and bank.vault == vault is validated inline
// token_index and bank.vault == vault is validated inline at #3
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
#[account(mut)]
@ -74,17 +76,16 @@ pub fn serum3_liq_force_cancel_orders(
{
let account = ctx.accounts.account.load()?;
// Validate open_orders
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)
.ok_or_else(|| error!(MangoError::SomeError))?
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
// Validate banks and vaults
// Validate banks and vaults #3
let quote_bank = ctx.accounts.quote_bank.load()?;
require!(
quote_bank.vault == ctx.accounts.quote_vault.key(),
@ -105,16 +106,58 @@ pub fn serum3_liq_force_cancel_orders(
);
}
// TODO: do the correct health / being_liquidated check
{
let account = ctx.accounts.account.load()?;
//
// Check liqee health if liquidation is allowed
//
let mut health_cache = {
let mut account = ctx.accounts.account.load_mut()?;
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Maint, &retriever)?;
msg!("health: {}", health);
require!(health < 0, MangoError::SomeError);
}
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
if account.being_liquidated() {
let init_health = health_cache.health(HealthType::Init);
if account
.fixed
.maybe_recover_from_being_liquidated(init_health)
{
msg!("Liqee init_health above zero");
return Ok(());
}
} else {
let maint_health = health_cache.health(HealthType::Maint);
require!(
maint_health < I80F48::ZERO,
MangoError::HealthMustBeNegative
);
account.fixed.set_being_liquidated(true);
}
health_cache
};
//
// Charge any open loan origination fees
//
let before_oo = {
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
let before_oo = OpenOrdersSlim::from_oo(&open_orders);
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()?;
charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
serum_market.market_index,
&mut base_bank,
&mut quote_bank,
&mut account.borrow_mut(),
&before_oo,
)?;
before_oo
};
//
// Before-settle tracking
@ -131,45 +174,51 @@ pub fn serum3_liq_force_cancel_orders(
//
// After-settle tracking
//
{
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
OODifference::new(&before_oo, &after_oo)
.adjust_health_cache(&mut health_cache, &serum_market)?;
};
ctx.accounts.base_vault.reload()?;
ctx.accounts.quote_vault.reload()?;
let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account
// Settle cannot decrease vault balances
require_gte!(after_base_vault, before_base_vault);
require_gte!(after_quote_vault, before_quote_vault);
// Credit 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 mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) =
apply_vault_difference(
&mut account.borrow_mut(),
&mut base_bank,
after_base_vault,
before_base_vault,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?;
vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut());
apply_vault_difference(
&mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank,
after_base_vault,
before_base_vault,
)?
.adjust_health_cache(&mut health_cache)?;
apply_vault_difference(
&mut account.borrow_mut(),
serum_market.market_index,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?
.adjust_health_cache(&mut health_cache)?;
if base_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: serum_market.base_token_index,
loan_origination_fee: base_loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders
});
}
if quote_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: serum_market.quote_token_index,
loan_origination_fee: quote_loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders
});
}
//
// Health check at the end
//
let init_health = health_cache.health(HealthType::Init);
account
.fixed
.maybe_recover_from_being_liquidated(init_health);
Ok(())
}

View File

@ -1,6 +1,6 @@
use crate::error::*;
use crate::serum3_cpi::load_open_orders_ref;
use crate::serum3_cpi::{load_market_state, load_open_orders_ref};
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
@ -9,17 +9,16 @@ use fixed::types::I80F48;
use num_enum::IntoPrimitive;
use num_enum::TryFromPrimitive;
use serum_dex::instruction::NewOrderInstructionV3;
use serum_dex::matching::Side;
use serum_dex::state::OpenOrders;
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
/// For loan origination fees bookkeeping purposes
#[derive(Debug)]
pub struct OpenOrdersSlim {
pub native_coin_free: u64,
pub native_coin_total: u64,
pub native_pc_free: u64,
pub native_pc_total: u64,
native_coin_free: u64,
native_coin_total: u64,
native_pc_free: u64,
native_pc_total: u64,
referrer_rebates_accrued: u64,
}
impl OpenOrdersSlim {
pub fn from_oo(oo: &OpenOrders) -> Self {
@ -28,30 +27,66 @@ impl OpenOrdersSlim {
native_coin_total: oo.native_coin_total,
native_pc_free: oo.native_pc_free,
native_pc_total: oo.native_pc_total,
referrer_rebates_accrued: oo.referrer_rebates_accrued,
}
}
}
pub trait OpenOrdersReserved {
fn native_coin_reserved(&self) -> u64;
fn native_pc_reserved(&self) -> u64;
pub trait OpenOrdersAmounts {
fn native_base_reserved(&self) -> u64;
fn native_quote_reserved(&self) -> u64;
fn native_base_free(&self) -> u64;
fn native_quote_free(&self) -> u64;
fn native_quote_free_plus_rebates(&self) -> u64;
fn native_base_total(&self) -> u64;
fn native_quote_total_plus_rebates(&self) -> u64;
}
impl OpenOrdersReserved for OpenOrdersSlim {
fn native_coin_reserved(&self) -> u64 {
self.native_coin_total - self.native_coin_free
impl OpenOrdersAmounts for OpenOrdersSlim {
fn native_base_reserved(&self) -> u64 {
cm!(self.native_coin_total - self.native_coin_free)
}
fn native_pc_reserved(&self) -> u64 {
self.native_pc_total - self.native_pc_free
fn native_quote_reserved(&self) -> u64 {
cm!(self.native_pc_total - self.native_pc_free)
}
fn native_base_free(&self) -> u64 {
self.native_coin_free
}
fn native_quote_free(&self) -> u64 {
self.native_pc_free
}
fn native_quote_free_plus_rebates(&self) -> u64 {
cm!(self.native_pc_free + self.referrer_rebates_accrued)
}
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
}
impl OpenOrdersReserved for OpenOrders {
fn native_coin_reserved(&self) -> u64 {
self.native_coin_total - self.native_coin_free
impl OpenOrdersAmounts for OpenOrders {
fn native_base_reserved(&self) -> u64 {
cm!(self.native_coin_total - self.native_coin_free)
}
fn native_pc_reserved(&self) -> u64 {
self.native_pc_total - self.native_pc_free
fn native_quote_reserved(&self) -> u64 {
cm!(self.native_pc_total - self.native_pc_free)
}
fn native_base_free(&self) -> u64 {
self.native_coin_free
}
fn native_quote_free(&self) -> u64 {
self.native_pc_free
}
fn native_quote_free_plus_rebates(&self) -> u64 {
cm!(self.native_pc_free + self.referrer_rebates_accrued)
}
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
}
@ -85,12 +120,16 @@ pub enum Serum3Side {
pub struct Serum3PlaceOrder<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(
@ -129,20 +168,13 @@ pub struct Serum3PlaceOrder<'info> {
/// CHECK: Validated by the serum cpi call
pub market_vault_signer: UncheckedAccount<'info>,
// TODO: do we need to pass both, or just payer?
// TODO: if we potentially settle immediately, they all need to be mut?
// TODO: Can we reduce the number of accounts by requiring the banks
// to be in the remainingAccounts (where they need to be anyway, for
// health checks - but they need to be mut)
// token_index and bank.vault == vault is validated inline
/// The bank that pays for the order, if necessary
// token_index and payer_bank.vault == payer_vault is validated inline at #3
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
pub payer_bank: AccountLoader<'info, Bank>,
/// The bank vault that pays for the order, if necessary
#[account(mut)]
pub quote_vault: Box<Account<'info, TokenAccount>>,
#[account(mut, has_one = group)]
pub base_bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub base_vault: Box<Account<'info, TokenAccount>>,
pub payer_vault: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
}
@ -166,68 +198,86 @@ pub fn serum3_place_order(
//
{
let account = ctx.accounts.account.load()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
);
// Validate open_orders
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)
.ok_or_else(|| error!(MangoError::SomeError))?
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
// Validate banks and vaults
let quote_bank = ctx.accounts.quote_bank.load()?;
require!(
quote_bank.vault == ctx.accounts.quote_vault.key(),
MangoError::SomeError
);
require!(
quote_bank.token_index == serum_market.quote_token_index,
MangoError::SomeError
);
let base_bank = ctx.accounts.base_bank.load()?;
require!(
base_bank.vault == ctx.accounts.base_vault.key(),
MangoError::SomeError
);
require!(
base_bank.token_index == serum_market.base_token_index,
MangoError::SomeError
);
// Validate bank and vault #3
let payer_bank = ctx.accounts.payer_bank.load()?;
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
let payer_token_index = match side {
Serum3Side::Bid => serum_market.quote_token_index,
Serum3Side::Ask => serum_market.base_token_index,
};
require_eq!(payer_bank.token_index, payer_token_index);
}
//
// Pre-health computation
//
let mut account = ctx.accounts.account.load_mut()?;
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
let pre_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_health))
} else {
None
};
//
// Before-order tracking
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
let before_vault = ctx.accounts.payer_vault.amount;
let before_oo = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
OpenOrdersSlim::from_oo(&open_orders)
};
// Provide a readable error message in case the vault doesn't have enough tokens
let (vault_amount, needed_amount) = match side {
Serum3Side::Ask => (before_base_vault, max_base_qty),
Serum3Side::Bid => (before_quote_vault, max_native_quote_qty_including_fees),
};
if vault_amount < needed_amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
needed_amount, vault_amount
)
});
{
let base_lot_size = load_market_state(
&ctx.accounts.serum_market_external,
&ctx.accounts.serum_program.key(),
)?
.coin_lot_size;
let needed_amount = match side {
Serum3Side::Ask => {
cm!(max_base_qty * base_lot_size).saturating_sub(before_oo.native_base_free())
}
Serum3Side::Bid => {
max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free())
}
};
if before_vault < needed_amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
needed_amount, before_vault
)
});
}
}
// TODO: pre-health check
//
// Apply the order to serum. Also immediately settle, in case the order
// matched against an existing other order.
// Apply the order to serum
//
let order = serum_dex::instruction::NewOrderInstructionV3 {
side: u8::try_from(side).unwrap().try_into().unwrap(),
@ -242,171 +292,149 @@ pub fn serum3_place_order(
client_order_id,
limit,
};
let before_oo = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
OpenOrdersSlim::from_oo(&open_orders)
};
cpi_place_order(ctx.accounts, order)?;
{
let oo_difference = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
let mut account = ctx.accounts.account.load_mut()?;
inc_maybe_loan(
serum_market.market_index,
&mut account.borrow_mut(),
&before_oo,
&after_oo,
);
}
cpi_settle_funds(ctx.accounts)?;
OODifference::new(&before_oo, &after_oo)
};
//
// After-order tracking
//
ctx.accounts.base_vault.reload()?;
ctx.accounts.quote_vault.reload()?;
let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount;
ctx.accounts.payer_vault.reload()?;
let after_vault = ctx.accounts.payer_vault.amount;
// Charge the difference in vault balances to the user's account
let mut account = ctx.accounts.account.load_mut()?;
let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = {
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
// Placing an order cannot increase vault balance
require_gte!(before_vault, after_vault);
// Charge the difference in vault balance to the user's account
let vault_difference = {
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
apply_vault_difference(
&mut account.borrow_mut(),
&mut base_bank,
after_base_vault,
before_base_vault,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
serum_market.market_index,
&mut payer_bank,
after_vault,
before_vault,
)?
};
//
// Health check
//
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);
account.fixed.maybe_recover_from_being_liquidated(health);
vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut());
if base_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: serum_market.base_token_index,
loan_origination_fee: base_loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder
});
}
if quote_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: serum_market.quote_token_index,
loan_origination_fee: quote_loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder
});
if let Some((mut health_cache, pre_health)) = pre_health_opt {
vault_difference.adjust_health_cache(&mut health_cache)?;
oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?;
account.check_health_post(&health_cache, pre_health)?;
}
Ok(())
}
// if reserved has increased, then increase cached value by the increase in reserved
pub fn inc_maybe_loan(
market_index: Serum3MarketIndex,
account: &mut MangoAccountRefMut,
before_oo: &OpenOrdersSlim,
after_oo: &OpenOrdersSlim,
) {
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
if after_oo.native_coin_reserved() > before_oo.native_coin_reserved() {
let native_coin_reserved_increase =
after_oo.native_coin_reserved() - before_oo.native_coin_reserved();
serum3_account.previous_native_coin_reserved =
cm!(serum3_account.previous_native_coin_reserved + native_coin_reserved_increase);
}
if after_oo.native_pc_reserved() > before_oo.native_pc_reserved() {
let reserved_pc_increase = after_oo.native_pc_reserved() - before_oo.native_pc_reserved();
serum3_account.previous_native_pc_reserved =
cm!(serum3_account.previous_native_pc_reserved + reserved_pc_increase);
}
pub struct OODifference {
reserved_base_change: I80F48,
reserved_quote_change: I80F48,
free_base_change: I80F48,
free_quote_change: I80F48,
}
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 MangoAccountRefMut) {
if !self.base_active {
account.deactivate_token_position(self.base_raw_index);
}
if !self.quote_active {
account.deactivate_token_position(self.quote_raw_index);
impl OODifference {
pub fn new(before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim) -> Self {
Self {
reserved_base_change: cm!(I80F48::from(after_oo.native_base_reserved())
- I80F48::from(before_oo.native_base_reserved())),
reserved_quote_change: cm!(I80F48::from(after_oo.native_quote_reserved())
- I80F48::from(before_oo.native_quote_reserved())),
free_base_change: cm!(I80F48::from(after_oo.native_base_free())
- I80F48::from(before_oo.native_base_free())),
free_quote_change: cm!(I80F48::from(after_oo.native_quote_free_plus_rebates())
- I80F48::from(before_oo.native_quote_free_plus_rebates())),
}
}
pub fn adjust_health_cache(
&self,
health_cache: &mut HealthCache,
market: &Serum3Market,
) -> Result<()> {
health_cache.adjust_serum3_reserved(
market.market_index,
market.base_token_index,
self.reserved_base_change,
self.free_base_change,
market.quote_token_index,
self.reserved_quote_change,
self.free_quote_change,
)
}
}
pub struct VaultDifference {
token_index: TokenIndex,
native_change: I80F48,
}
impl VaultDifference {
pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> {
health_cache.adjust_token_balance(self.token_index, self.native_change)?;
Ok(())
}
}
/// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after
/// changing the vault balances
pub fn apply_vault_difference(
account: &mut MangoAccountRefMut,
base_bank: &mut Bank,
after_base_vault: u64,
before_base_vault: u64,
quote_bank: &mut Bank,
after_quote_vault: u64,
before_quote_vault: u64,
) -> Result<(VaultDifferenceResult, I80F48, I80F48)> {
// 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!
serum_market_index: Serum3MarketIndex,
bank: &mut Bank,
vault_after: u64,
vault_before: u64,
) -> Result<VaultDifference> {
let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before));
let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?;
let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault);
let (base_active, base_loan_origination_fee) =
base_bank.change_with_fee(base_position, base_change)?;
let (position, _) = account.token_position_mut(bank.token_index)?;
let native_before = position.native(bank);
bank.change_without_fee(position, needed_change)?;
let native_after = position.native(bank);
let native_change = cm!(native_after - native_before);
let new_borrows = native_change
.max(native_after)
.min(I80F48::ZERO)
.abs()
.to_num::<u64>();
let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?;
let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault);
let (quote_active, quote_loan_origination_fee) =
quote_bank.change_with_fee(quote_position, quote_change)?;
let market = account.serum3_orders_mut(serum_market_index).unwrap();
let borrows_without_fee = if bank.token_index == market.base_token_index {
&mut market.base_borrows_without_fee
} else if bank.token_index == market.quote_token_index {
&mut market.quote_borrows_without_fee
} else {
return Err(error_msg!(
"assert failed: apply_vault_difference called with bad token index"
));
};
Ok((
VaultDifferenceResult {
base_raw_index,
base_active,
quote_raw_index,
quote_active,
},
base_loan_origination_fee,
quote_loan_origination_fee,
))
// Only for place: Add to potential borrow amount
let old_value = *borrows_without_fee;
*borrows_without_fee = cm!(old_value + new_borrows);
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
if needed_change > 0 {
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
}
Ok(VaultDifference {
token_index: bank.token_index,
native_change,
})
}
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
use crate::serum3_cpi;
let order_payer_token_account = match order.side {
Side::Bid => &ctx.quote_vault,
Side::Ask => &ctx.base_vault,
};
let group = ctx.group.load()?;
serum3_cpi::PlaceOrder {
program: ctx.serum_program.to_account_info(),
@ -420,26 +448,8 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu
token_program: ctx.token_program.to_account_info(),
open_orders: ctx.open_orders.to_account_info(),
order_payer_token_account: order_payer_token_account.to_account_info(),
order_payer_token_account: ctx.payer_vault.to_account_info(),
user_authority: ctx.group.to_account_info(),
}
.call(&group, order)
}
fn cpi_settle_funds(ctx: &Serum3PlaceOrder) -> Result<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;
serum3_cpi::SettleFunds {
program: ctx.serum_program.to_account_info(),
market: ctx.serum_market_external.to_account_info(),
open_orders: ctx.open_orders.to_account_info(),
open_orders_authority: ctx.group.to_account_info(),
base_vault: ctx.market_base_vault.to_account_info(),
quote_vault: ctx.market_quote_vault.to_account_info(),
user_base_wallet: ctx.base_vault.to_account_info(),
user_quote_wallet: ctx.quote_vault.to_account_info(),
vault_signer: ctx.market_vault_signer.to_account_info(),
token_program: ctx.token_program.to_account_info(),
}
.call(&group)
}

View File

@ -6,6 +6,7 @@ use crate::state::*;
use crate::util::fill_from_str;
#[derive(Accounts)]
#[instruction(market_index: Serum3MarketIndex)]
pub struct Serum3RegisterMarket<'info> {
#[account(
mut,
@ -15,7 +16,6 @@ pub struct Serum3RegisterMarket<'info> {
pub group: AccountLoader<'info, Group>,
pub admin: Signer<'info>,
// TODO: limit?
/// CHECK: Can register a market for any serum program
pub serum_program: UncheckedAccount<'info>,
/// CHECK: Can register any serum market
@ -31,6 +31,17 @@ pub struct Serum3RegisterMarket<'info> {
)]
pub serum_market: AccountLoader<'info, Serum3Market>,
/// CHECK: Unused account
#[account(
init,
// block using the same market index twice
seeds = [b"Serum3Index".as_ref(), group.key().as_ref(), &market_index.to_le_bytes()],
bump,
payer = payer,
space = 1,
)]
pub index_reservation: UncheckedAccount<'info>,
#[account(has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
#[account(has_one = group)]
@ -42,7 +53,6 @@ pub struct Serum3RegisterMarket<'info> {
pub system_program: Program<'info, System>,
}
// TODO: should this be "configure_serum_market", which allows reconfiguring?
pub fn serum3_register_market(
ctx: Context<Serum3RegisterMarket>,
market_index: Serum3MarketIndex,

View File

@ -1,5 +1,3 @@
use std::borrow::BorrowMut;
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
@ -9,19 +7,23 @@ use crate::error::*;
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
use super::{apply_vault_difference, OpenOrdersReserved, OpenOrdersSlim};
use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim};
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
#[derive(Accounts)]
pub struct Serum3SettleFunds<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(
@ -48,7 +50,7 @@ pub struct Serum3SettleFunds<'info> {
/// CHECK: Validated by the serum cpi call
pub market_vault_signer: UncheckedAccount<'info>,
// token_index and bank.vault == vault is validated inline
// token_index and bank.vault == vault is validated inline at #3
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
#[account(mut)]
@ -74,22 +76,22 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
//
{
let account = ctx.accounts.account.load()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
);
// Validate open_orders
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)
.ok_or_else(|| error!(MangoError::SomeError))?
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
// Validate banks and vaults
// Validate banks and vaults #3
let quote_bank = ctx.accounts.quote_bank.load()?;
require!(
quote_bank.vault == ctx.accounts.quote_vault.key(),
@ -111,34 +113,35 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
}
//
// Before-order tracking
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
//
// Settle
// Charge any open loan origination fees
//
{
let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?;
cpi_settle_funds(ctx.accounts)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
let before_oo = OpenOrdersSlim::from_oo(&open_orders);
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()?;
charge_maybe_fees(
charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
serum_market.market_index,
&mut base_bank,
&mut quote_bank,
&mut account.borrow_mut(),
&after_oo,
&before_oo,
)?;
}
//
// After-order tracking
// Settle
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
cpi_settle_funds(ctx.accounts)?;
//
// After-settle tracking
//
{
ctx.accounts.base_vault.reload()?;
@ -146,102 +149,92 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount;
// Charge the difference in vault balances to the user's account
// Settle cannot decrease vault balances
require_gte!(after_base_vault, before_base_vault);
require_gte!(after_quote_vault, before_quote_vault);
// Credit 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 mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) =
apply_vault_difference(
&mut account.borrow_mut(),
&mut base_bank,
after_base_vault,
before_base_vault,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?;
vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut());
if base_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: serum_market.base_token_index,
loan_origination_fee: base_loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds
});
}
if quote_loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: serum_market.quote_token_index,
loan_origination_fee: quote_loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds
});
}
apply_vault_difference(
&mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank,
after_base_vault,
before_base_vault,
)?;
apply_vault_difference(
&mut account.borrow_mut(),
serum_market.market_index,
&mut quote_bank,
after_quote_vault,
before_quote_vault,
)?;
}
Ok(())
}
// if reserved is less than cached, charge loan fee on the difference
pub fn charge_maybe_fees(
// Charge fees if the potential borrows are bigger than the funds on the open orders account
pub fn charge_loan_origination_fees(
group_pubkey: &Pubkey,
account_pubkey: &Pubkey,
market_index: Serum3MarketIndex,
coin_bank: &mut Bank,
pc_bank: &mut Bank,
base_bank: &mut Bank,
quote_bank: &mut Bank,
account: &mut MangoAccountRefMut,
after_oo: &OpenOrdersSlim,
before_oo: &OpenOrdersSlim,
) -> Result<()> {
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
let maybe_actualized_coin_loan = I80F48::from_num::<u64>(
let oo_base_total = before_oo.native_base_total();
let actualized_base_loan = I80F48::from_num(
serum3_account
.previous_native_coin_reserved
.saturating_sub(after_oo.native_coin_reserved()),
.base_borrows_without_fee
.saturating_sub(oo_base_total),
);
if actualized_base_loan > 0 {
serum3_account.base_borrows_without_fee = oo_base_total;
if maybe_actualized_coin_loan > 0 {
serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved();
// now that the loan is actually materialized, charge the loan origination fee
// note: the withdraw has already happened while placing the order
let base_token_account = account.token_position_mut(base_bank.token_index)?.0;
let (_, fee) =
base_bank.withdraw_loan_origination_fee(base_token_account, actualized_base_loan)?;
// loan origination fees
let coin_token_account = account.token_position_mut(coin_bank.token_index)?.0;
let coin_token_native = coin_token_account.native(coin_bank);
if coin_token_native.is_negative() {
let actualized_loan = coin_token_native.abs().min(maybe_actualized_coin_loan);
// note: the withdraw has already happened while placing the order
// now that the loan is actually materialized (since the fill having taken place)
// charge the loan origination fee
coin_bank
.borrow_mut()
.withdraw_loan_origination_fee(coin_token_account, actualized_loan)?;
}
emit!(WithdrawLoanOriginationFeeLog {
mango_group: *group_pubkey,
mango_account: *account_pubkey,
token_index: base_bank.token_index,
loan_origination_fee: fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
});
}
let serum3_account = account.serum3_orders_mut(market_index).unwrap();
let maybe_actualized_pc_loan = I80F48::from_num::<u64>(
let oo_quote_total = before_oo.native_quote_total_plus_rebates();
let actualized_quote_loan = I80F48::from_num::<u64>(
serum3_account
.previous_native_pc_reserved
.saturating_sub(after_oo.native_pc_reserved()),
.quote_borrows_without_fee
.saturating_sub(oo_quote_total),
);
if actualized_quote_loan > 0 {
serum3_account.quote_borrows_without_fee = oo_quote_total;
if maybe_actualized_pc_loan > 0 {
serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved();
// now that the loan is actually materialized, charge the loan origination fee
// note: the withdraw has already happened while placing the order
let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0;
let (_, fee) =
quote_bank.withdraw_loan_origination_fee(quote_token_account, actualized_quote_loan)?;
// loan origination fees
let pc_token_account = account.token_position_mut(pc_bank.token_index)?.0;
let pc_token_native = pc_token_account.native(pc_bank);
if pc_token_native.is_negative() {
let actualized_loan = pc_token_native.abs().min(maybe_actualized_pc_loan);
// note: the withdraw has already happened while placing the order
// now that the loan is actually materialized (since the fill having taken place)
// charge the loan origination fee
pc_bank
.borrow_mut()
.withdraw_loan_origination_fee(pc_token_account, actualized_loan)?;
}
emit!(WithdrawLoanOriginationFeeLog {
mango_group: *group_pubkey,
mango_account: *account_pubkey,
token_index: quote_bank.token_index,
loan_origination_fee: fee.to_bits(),
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
});
}
Ok(())

View File

@ -4,6 +4,7 @@ use anchor_spl::token::Token;
use anchor_spl::token::TokenAccount;
use fixed::types::I80F48;
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::state::*;
use crate::util::checked_math as cm;
@ -21,6 +22,7 @@ pub struct TokenDeposit<'info> {
mut,
has_one = group,
has_one = vault,
has_one = oracle,
// the mints of bank/vault/token_account are implicitly the same because
// spl::token::transfer succeeds between token_account and vault
)]
@ -29,6 +31,9 @@ pub struct TokenDeposit<'info> {
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
#[account(mut)]
pub token_account: Box<Account<'info, TokenAccount>>,
pub token_authority: Signer<'info>,
@ -56,7 +61,7 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
let (position, raw_token_index, active_token_index) =
let (position, raw_token_index, _active_token_index) =
account.ensure_token_position(token_index)?;
let amount_i80f48 = I80F48::from(amount);
@ -69,10 +74,8 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
let indexed_position = position.indexed_position;
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let (bank, oracle_price) =
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
let bank = ctx.accounts.bank.load()?;
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
@ -91,10 +94,17 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
//
// Health computation
//
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
account.fixed.maybe_recover_from_being_liquidated(health);
// Since depositing can only increase health, we can skip the usual pre-health computation.
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
//
if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
account.fixed.maybe_recover_from_being_liquidated(health);
}
//
// Deactivate the position only after the health check because the user passed in

View File

@ -4,8 +4,9 @@ use anchor_spl::token::{self, CloseAccount, Token, TokenAccount};
use crate::{accounts_zerocopy::LoadZeroCopyRef, state::*};
use anchor_lang::AccountsClose;
/// In addition to these accounts, there must be remaining_accounts:
/// all n pairs of bank and its corresponding vault account for a token
#[derive(Accounts)]
#[instruction(token_index: TokenIndex)]
pub struct TokenDeregister<'info> {
#[account(
constraint = group.load()?.is_testing(),
@ -18,7 +19,6 @@ pub struct TokenDeregister<'info> {
#[account(
mut,
has_one = group,
constraint = mint_info.load()?.token_index == token_index,
close = sol_destination
)]
pub mint_info: AccountLoader<'info, MintInfo>,
@ -36,7 +36,6 @@ pub struct TokenDeregister<'info> {
#[allow(clippy::too_many_arguments)]
pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>,
token_index: TokenIndex,
) -> Result<()> {
let mint_info = ctx.accounts.mint_info.load()?;
{
@ -61,7 +60,7 @@ pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
{
let bank = bank_ai.load::<Bank>()?;
require_keys_eq!(bank.group, ctx.accounts.group.key());
require_eq!(bank.token_index, token_index);
require_eq!(bank.token_index, mint_info.token_index);
require_keys_eq!(bank.vault, vault_ai.key());
}

View File

@ -5,7 +5,7 @@ use crate::logs::{UpdateIndexLog, UpdateRateLog};
use crate::state::HOUR;
use crate::{
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
state::{oracle_price, Bank, Group, MintInfo},
state::{Bank, Group, MintInfo},
};
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use anchor_lang::Discriminator;
@ -106,11 +106,8 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
now_ts,
);
let price = oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
some_bank.oracle_config.conf_filter,
some_bank.mint_decimals,
)?;
let price =
some_bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
emit!(UpdateIndexLog {
mango_group: mint_info.group.key(),
token_index: mint_info.token_index,

View File

@ -1,3 +1,4 @@
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
@ -9,7 +10,6 @@ use fixed::types::I80F48;
use crate::logs::{
LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanOriginationFeeLog, WithdrawLog,
};
use crate::state::new_fixed_order_account_retriever;
use crate::util::checked_math as cm;
#[derive(Accounts)]
@ -24,6 +24,7 @@ pub struct TokenWithdraw<'info> {
mut,
has_one = group,
has_one = vault,
has_one = oracle,
// the mints of bank/vault/token_account are implicitly the same because
// spl::token::transfer succeeds between token_account and vault
)]
@ -32,6 +33,9 @@ pub struct TokenWithdraw<'info> {
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
#[account(mut)]
pub token_account: Box<Account<'info, TokenAccount>>,
@ -56,90 +60,92 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let group = ctx.accounts.group.load()?;
let token_index = ctx.accounts.bank.load()?.token_index;
// Get the account's position for that token index
// Create the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
let (_, raw_token_index, _) = account.ensure_token_position(token_index)?;
let (position, raw_token_index, active_token_index) =
account.ensure_token_position(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, amount_i80f48, loan_origination_fee) = {
let mut bank = ctx.accounts.bank.load_mut()?;
let native_position = position.native(&bank);
// Handle amount special case for withdrawing everything
let amount = if amount == u64::MAX && !allow_borrow {
if native_position.is_positive() {
// TODO: This rounding may mean that if we deposit and immediately withdraw
// we can't withdraw the full amount!
native_position.floor().to_num::<u64>()
} else {
return Ok(());
}
} else {
amount
};
require!(
allow_borrow || amount <= native_position,
MangoError::SomeError
);
let amount_i80f48 = I80F48::from(amount);
// Update the bank and position
let (position_is_active, loan_origination_fee) =
bank.withdraw_with_fee(position, amount_i80f48)?;
// Provide a readable error message in case the vault doesn't have enough tokens
if ctx.accounts.vault.amount < amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
amount, ctx.accounts.vault.amount
)
});
}
// Transfer the actual tokens
let group_seeds = group_seeds!(group);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
amount,
)?;
(position_is_active, amount_i80f48, loan_origination_fee)
// Health check _after_ the token position is guaranteed to exist
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
let pre_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_health))
} else {
None
};
let indexed_position = position.indexed_position;
let mut bank = ctx.accounts.bank.load_mut()?;
let position = account.token_position_mut_by_raw_index(raw_token_index);
let native_position = position.native(&bank);
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let (bank, oracle_price) =
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
// Handle amount special case for withdrawing everything
let amount = if amount == u64::MAX && !allow_borrow {
if native_position.is_positive() {
// TODO: This rounding may mean that if we deposit and immediately withdraw
// we can't withdraw the full amount!
native_position.floor().to_num::<u64>()
} else {
return Ok(());
}
} else {
amount
};
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd);
require!(
allow_borrow || amount <= native_position,
MangoError::SomeError
);
let amount_i80f48 = I80F48::from(amount);
// Update the bank and position
let (position_is_active, loan_origination_fee) =
bank.withdraw_with_fee(position, amount_i80f48)?;
// Provide a readable error message in case the vault doesn't have enough tokens
if ctx.accounts.vault.amount < amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
amount, ctx.accounts.vault.amount
)
});
}
// Transfer the actual tokens
let group_seeds = group_seeds!(group);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
amount,
)?;
let native_position_after = position.native(&bank);
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index,
indexed_position: indexed_position.to_bits(),
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: oracle_price.to_bits(),
});
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd);
//
// Health check
//
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-withdraw init health")?;
msg!("health: {}", health);
require!(health >= 0, MangoError::HealthMustBePositive);
account.fixed.maybe_recover_from_being_liquidated(health);
if let Some((mut health_cache, pre_health)) = pre_health_opt {
health_cache
.adjust_token_balance(token_index, cm!(native_position_after - native_position))?;
account.check_health_post(&health_cache, pre_health)?;
}
//
// Deactivate the position only after the health check because the user passed in

View File

@ -142,9 +142,8 @@ pub mod mango_v4 {
pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>,
token_index: TokenIndex,
) -> Result<()> {
instructions::token_deregister(ctx, token_index)
instructions::token_deregister(ctx)
}
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
@ -236,6 +235,18 @@ pub mod mango_v4 {
instructions::flash_loan_end(ctx, flash_loan_type)
}
pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionBegin<'info>>,
) -> Result<()> {
instructions::health_region_begin(ctx)
}
pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>,
) -> Result<()> {
instructions::health_region_end(ctx)
}
///
/// Serum
///

View File

@ -1,4 +1,6 @@
use super::{OracleConfig, TokenIndex, TokenPosition};
use crate::accounts_zerocopy::KeyedAccountReader;
use crate::state::oracle;
use crate::util;
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
@ -274,7 +276,7 @@ impl Bank {
let (position_is_active, _) =
self.withdraw_internal(position, native_amount, false, !position.is_in_use())?;
return Ok(position_is_active);
Ok(position_is_active)
}
/// Like `withdraw_without_fee()` but allows dusting of in-use token accounts.
@ -285,9 +287,8 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
) -> Result<bool> {
Ok(self
.withdraw_internal(position, native_amount, false, true)
.map(|(not_dusted, _)| not_dusted || position.is_in_use())?)
self.withdraw_internal(position, native_amount, false, true)
.map(|(not_dusted, _)| not_dusted || position.is_in_use())
}
/// Withdraws `native_amount` while applying the loan origination fee if a borrow is created.
@ -545,6 +546,15 @@ impl Bank {
(self.rate0, self.rate1, self.max_rate)
}
}
pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result<I80F48> {
require_keys_eq!(self.oracle, *oracle_acc.key());
oracle::oracle_price(
oracle_acc,
self.oracle_config.conf_filter,
self.mint_decimals,
)
}
}
#[macro_export]

View File

@ -44,7 +44,7 @@ impl Group {
}
pub fn multiple_banks_supported(&self) -> bool {
self.is_testing() || self.version > 0
self.is_testing() || self.version > 1
}
pub fn serum3_supported(&self) -> bool {
@ -52,7 +52,7 @@ impl Group {
}
pub fn perps_supported(&self) -> bool {
self.is_testing() || self.version > 0
self.is_testing() || self.version > 1
}
}

View File

@ -11,7 +11,10 @@ use std::collections::HashMap;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::serum3_cpi;
use crate::state::{oracle_price, Bank, PerpMarket, PerpMarketIndex, TokenIndex};
use crate::state::{
Bank, MangoAccountFixed, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex,
TokenIndex,
};
use crate::util::checked_math as cm;
use super::MangoAccountRef;
@ -89,8 +92,7 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
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());
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)
bank.oracle_price(oracle)
}
}
@ -275,8 +277,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let index = self.bank_index(token_index1)?;
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
require_keys_eq!(bank.oracle, *oracle.key);
let price = oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?;
let price = bank.oracle_price(oracle)?;
return Ok((bank, price, None));
}
let index1 = self.bank_index(token_index1)?;
@ -294,12 +295,8 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
let oracle1 = &self.oracles[first];
let oracle2 = &self.oracles[second];
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)?;
let price1 = bank1.oracle_price(oracle1)?;
let price2 = bank2.oracle_price(oracle2)?;
if swap {
Ok((bank2, price2, Some((bank1, price1))))
} else {
@ -311,11 +308,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let index = self.bank_index(token_index)?;
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
require_keys_eq!(bank.oracle, *oracle.key);
Ok((
bank,
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?,
))
Ok((bank, bank.oracle_price(oracle)?))
}
pub fn scanned_perp_market(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpMarket> {
@ -437,6 +430,7 @@ pub struct Serum3Info {
reserved: I80F48,
base_index: usize,
quote_index: usize,
market_index: Serum3MarketIndex,
}
impl Serum3Info {
@ -480,6 +474,7 @@ impl Serum3Info {
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
pub struct PerpInfo {
perp_market_index: PerpMarketIndex,
maint_asset_weight: I80F48,
init_asset_weight: I80F48,
maint_liab_weight: I80F48,
@ -491,6 +486,93 @@ pub struct PerpInfo {
}
impl PerpInfo {
fn new(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
token_infos: &[TokenInfo],
) -> Result<Self> {
// find the TokenInfos for the market's base and quote tokens
let base_index = find_token_info_index(token_infos, perp_market.base_token_index)?;
// TODO: base_index could be unset
let base_info = &token_infos[base_index];
let base_lot_size = I80F48::from(perp_market.base_lot_size);
let base_lots = cm!(perp_position.base_position_lots + perp_position.taker_base_lots);
let taker_quote = I80F48::from(cm!(
perp_position.taker_quote_lots * perp_market.quote_lot_size
));
let quote_current = cm!(perp_position.quote_position_native + taker_quote);
// Two scenarios:
// 1. The price goes low and all bids execute, converting to base.
// That means the perp position is increased by `bids` and the quote position
// is decreased by `bids * base_lot_size * price`.
// The health for this case is:
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
// 2. The price goes high and all asks execute, converting to quote.
// The health for this case is:
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
//
// Comparing these makes it clear we need to pick the worse subfactor
// weighted(base_lots + bids) - bids =: scenario1
// or
// weighted(base_lots - asks) + asks =: scenario2
//
// Additionally, we want this scenario choice to be the same no matter whether we're
// computing init or maint health. This can be guaranteed by requiring the weights
// to satisfy the property (P):
//
// (1 - init_asset_weight) / (init_liab_weight - 1)
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
//
// Derivation:
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
// Now
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
// scenario1 < scenario2
// becomes:
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
//
// We satisfy (P) by requiring
// asset_weight = 1 - x and liab_weight = 1 + x
//
// And with that assumption the scenario choice condition further simplifies to:
// scenario1 < scenario2
// iff abs(bids_net_lots) > abs(asks_net_lots)
let bids_net_lots = cm!(base_lots + perp_position.bids_base_lots);
let asks_net_lots = cm!(base_lots - perp_position.asks_base_lots);
let lots_to_quote = base_lot_size * base_info.oracle_price;
let base;
let quote;
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
let bids_net_lots = I80F48::from(bids_net_lots);
let bids_base_lots = I80F48::from(perp_position.bids_base_lots);
base = cm!(bids_net_lots * lots_to_quote);
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
} else {
let asks_net_lots = I80F48::from(asks_net_lots);
let asks_base_lots = I80F48::from(perp_position.asks_base_lots);
base = cm!(asks_net_lots * lots_to_quote);
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
};
Ok(Self {
perp_market_index: perp_market.perp_market_index,
init_asset_weight: perp_market.init_asset_weight,
init_liab_weight: perp_market.init_liab_weight,
maint_asset_weight: perp_market.maint_asset_weight,
maint_liab_weight: perp_market.maint_liab_weight,
base,
quote,
})
}
/// Total health contribution from perp balances
///
/// Due to isolation of perp markets, users may never borrow against perp
@ -536,16 +618,107 @@ impl HealthCache {
health
}
pub fn check_health_pre(&self, account: &mut MangoAccountFixed) -> Result<I80F48> {
let pre_health = self.health(HealthType::Init);
msg!("pre_health: {}", pre_health);
account.maybe_recover_from_being_liquidated(pre_health);
require!(!account.being_liquidated(), MangoError::BeingLiquidated);
Ok(pre_health)
}
pub fn check_health_post(
&self,
account: &mut MangoAccountFixed,
pre_health: I80F48,
) -> Result<()> {
let post_health = self.health(HealthType::Init);
msg!("post_health: {}", post_health);
require!(
post_health >= 0 || post_health > pre_health,
MangoError::HealthMustBePositiveOrIncrease
);
account.maybe_recover_from_being_liquidated(post_health);
Ok(())
}
fn token_entry_index(&mut self, token_index: TokenIndex) -> Result<usize> {
self.token_infos
.iter()
.position(|t| t.token_index == token_index)
.ok_or_else(|| error_msg!("token index {} not found", token_index))
}
pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> {
let mut entry = self
.token_infos
.iter_mut()
.find(|t| t.token_index == token_index)
.ok_or_else(|| error_msg!("token index {} not found", token_index))?;
let entry_index = self.token_entry_index(token_index)?;
let mut entry = &mut self.token_infos[entry_index];
entry.balance = cm!(entry.balance + change * entry.oracle_price);
Ok(())
}
pub fn adjust_serum3_reserved(
&mut self,
market_index: Serum3MarketIndex,
base_token_index: TokenIndex,
reserved_base_change: I80F48,
free_base_change: I80F48,
quote_token_index: TokenIndex,
reserved_quote_change: I80F48,
free_quote_change: I80F48,
) -> Result<()> {
let base_entry_index = self.token_entry_index(base_token_index)?;
let quote_entry_index = self.token_entry_index(quote_token_index)?;
// Compute the total reserved amount change in health reference units
let mut reserved_amount;
{
let base_entry = &mut self.token_infos[base_entry_index];
reserved_amount = cm!(reserved_base_change * base_entry.oracle_price);
}
{
let quote_entry = &mut self.token_infos[quote_entry_index];
reserved_amount =
cm!(reserved_amount + reserved_quote_change * quote_entry.oracle_price);
}
// Apply it to the tokens
{
let base_entry = &mut self.token_infos[base_entry_index];
base_entry.serum3_max_reserved = cm!(base_entry.serum3_max_reserved + reserved_amount);
base_entry.balance =
cm!(base_entry.balance + free_base_change * base_entry.oracle_price);
}
{
let quote_entry = &mut self.token_infos[quote_entry_index];
quote_entry.serum3_max_reserved =
cm!(quote_entry.serum3_max_reserved + reserved_amount);
quote_entry.balance =
cm!(quote_entry.balance + free_quote_change * quote_entry.oracle_price);
}
// Apply it to the serum3 info
let market_entry = self
.serum3_infos
.iter_mut()
.find(|m| m.market_index == market_index)
.ok_or_else(|| error_msg!("serum3 market {} not found", market_index))?;
market_entry.reserved = cm!(market_entry.reserved + reserved_amount);
Ok(())
}
pub fn recompute_perp_info(
&mut self,
perp_position: &PerpPosition,
perp_market: &PerpMarket,
) -> Result<()> {
let perp_entry = self
.perp_infos
.iter_mut()
.find(|m| m.perp_market_index == perp_market.perp_market_index)
.ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?;
*perp_entry = PerpInfo::new(perp_position, perp_market, &self.token_infos)?;
Ok(())
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self
.token_infos
@ -842,95 +1015,17 @@ pub fn new_health_cache(
reserved: reserved_balance,
base_index,
quote_index,
market_index: serum_account.market_index,
});
}
// TODO: also account for perp funding in health
// health contribution from perp accounts
let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count());
for (i, perp_account) in account.active_perp_positions().enumerate() {
for (i, perp_position) in account.active_perp_positions().enumerate() {
let perp_market =
retriever.perp_market(&account.fixed.group, i, perp_account.market_index)?;
// find the TokenInfos for the market's base and quote tokens
let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?;
// TODO: base_index could be unset
let base_info = &token_infos[base_index];
let base_lot_size = I80F48::from(perp_market.base_lot_size);
let base_lots = cm!(perp_account.base_position_lots + perp_account.taker_base_lots);
let taker_quote = I80F48::from(cm!(
perp_account.taker_quote_lots * perp_market.quote_lot_size
));
let quote_current = cm!(perp_account.quote_position_native + taker_quote);
// Two scenarios:
// 1. The price goes low and all bids execute, converting to base.
// That means the perp position is increased by `bids` and the quote position
// is decreased by `bids * base_lot_size * price`.
// The health for this case is:
// (weighted(base_lots + bids) - bids) * base_lot_size * price + quote
// 2. The price goes high and all asks execute, converting to quote.
// The health for this case is:
// (weighted(base_lots - asks) + asks) * base_lot_size * price + quote
//
// Comparing these makes it clear we need to pick the worse subfactor
// weighted(base_lots + bids) - bids =: scenario1
// or
// weighted(base_lots - asks) + asks =: scenario2
//
// Additionally, we want this scenario choice to be the same no matter whether we're
// computing init or maint health. This can be guaranteed by requiring the weights
// to satisfy the property (P):
//
// (1 - init_asset_weight) / (init_liab_weight - 1)
// == (1 - maint_asset_weight) / (maint_liab_weight - 1)
//
// Derivation:
// Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids.
// Now
// scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and
// scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots
// So with expanding weigthed(a) = weight_factor_for_a * a, the question
// scenario1 < scenario2
// becomes:
// (weight_factor_for_bids_net_lots - 1) * bids_net_lots
// < (weight_factor_for_asks_net_lots - 1) * asks_net_lots
// Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows.
//
// We satisfy (P) by requiring
// asset_weight = 1 - x and liab_weight = 1 + x
//
// And with that assumption the scenario choice condition further simplifies to:
// scenario1 < scenario2
// iff abs(bids_net_lots) > abs(asks_net_lots)
let bids_net_lots = cm!(base_lots + perp_account.bids_base_lots);
let asks_net_lots = cm!(base_lots - perp_account.asks_base_lots);
let lots_to_quote = base_lot_size * base_info.oracle_price;
let base;
let quote;
if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) {
let bids_net_lots = I80F48::from(bids_net_lots);
let bids_base_lots = I80F48::from(perp_account.bids_base_lots);
base = cm!(bids_net_lots * lots_to_quote);
quote = cm!(quote_current - bids_base_lots * lots_to_quote);
} else {
let asks_net_lots = I80F48::from(asks_net_lots);
let asks_base_lots = I80F48::from(perp_account.asks_base_lots);
base = cm!(asks_net_lots * lots_to_quote);
quote = cm!(quote_current + asks_base_lots * lots_to_quote);
};
perp_infos.push(PerpInfo {
init_asset_weight: perp_market.init_asset_weight,
init_liab_weight: perp_market.init_liab_weight,
maint_asset_weight: perp_market.maint_asset_weight,
maint_liab_weight: perp_market.maint_liab_weight,
base,
quote,
});
retriever.perp_market(&account.fixed.group, i, perp_position.market_index)?;
perp_infos.push(PerpInfo::new(perp_position, perp_market, &token_infos)?);
}
Ok(HealthCache {
@ -1064,7 +1159,7 @@ mod tests {
// Run a health test that includes all the side values (like referrer_rebates_accrued)
#[test]
fn test_health0() {
let buffer = MangoAccount::default().try_to_vec().unwrap();
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
let group = Pubkey::new_unique();
@ -1243,7 +1338,7 @@ mod tests {
expected_health: f64,
}
fn test_health1_runner(testcase: &TestHealth1Case) {
let buffer = MangoAccount::default().try_to_vec().unwrap();
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
let group = Pubkey::new_unique();
@ -1454,7 +1549,7 @@ mod tests {
for (i, testcase) in testcases.iter().enumerate() {
println!("checking testcase {}", i);
test_health1_runner(&testcase);
test_health1_runner(testcase);
}
}

View File

@ -17,11 +17,12 @@ use super::FillEvent;
use super::LeafNode;
use super::PerpMarket;
use super::PerpMarketIndex;
use super::PerpOpenOrders;
use super::PerpOpenOrder;
use super::Serum3MarketIndex;
use super::Side;
use super::TokenIndex;
use super::FREE_ORDER_SLOT;
use super::{HealthCache, HealthType};
use super::{PerpPosition, Serum3Orders, TokenPosition};
use checked_math as cm;
@ -56,9 +57,16 @@ pub struct MangoAccount {
/// Normally accounts can not be liquidated while maint_health >= 0. But when an account
/// reaches maint_health < 0, liquidators will call a liquidation instruction and thereby
/// set this flag. Now the account may be liquidated until init_health >= 0.
being_liquidated: u8,
///
/// Many actions should be disabled while the account is being liquidated, even if
/// its maint health has recovered to positive. Creating new open orders would, for example,
/// confuse liquidators.
pub being_liquidated: u8,
padding2: u8,
/// The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.
///
/// Must never be set after a transaction ends.
pub in_health_region: u8,
pub bump: u8,
@ -72,7 +80,10 @@ pub struct MangoAccount {
// TODO: unimplemented
pub net_settled: i64,
pub reserved: [u8; 248],
/// Init health as calculated during HealthReginBegin, rounded up.
pub health_region_pre_init_health: i64,
pub reserved: [u8; 240],
// dynamic
pub header_version: u8,
@ -89,24 +100,25 @@ pub struct MangoAccount {
pub padding6: u32,
pub perps: Vec<PerpPosition>,
pub padding7: u32,
pub perp_open_orders: Vec<PerpOpenOrders>,
pub perp_open_orders: Vec<PerpOpenOrder>,
}
impl Default for MangoAccount {
fn default() -> Self {
impl MangoAccount {
pub fn default_for_tests() -> Self {
Self {
name: Default::default(),
group: Pubkey::default(),
owner: Pubkey::default(),
delegate: Pubkey::default(),
being_liquidated: 0,
padding2: 0,
in_health_region: 0,
account_num: 0,
bump: 0,
padding: Default::default(),
net_deposits: 0,
net_settled: 0,
reserved: [0; 248],
health_region_pre_init_health: 0,
reserved: [0; 240],
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(),
padding4: Default::default(),
@ -114,14 +126,13 @@ impl Default for MangoAccount {
padding5: Default::default(),
serum3: vec![Serum3Orders::default(); 5],
padding6: Default::default(),
perps: vec![PerpPosition::default(); 2],
perps: vec![PerpPosition::default(); 4],
padding7: Default::default(),
perp_open_orders: vec![PerpOpenOrders::default(); 2],
perp_open_orders: vec![PerpOpenOrder::default(); 6],
}
}
}
impl MangoAccount {
/// Number of bytes needed for the MangoAccount, including the discriminator
pub fn space(
token_count: u8,
serum3_count: u8,
@ -167,59 +178,10 @@ impl MangoAccount {
perp_oo_count: u8,
) -> usize {
Self::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count)
+ (BORSH_VEC_SIZE_BYTES + size_of::<PerpOpenOrders>() * usize::from(perp_oo_count))
+ (BORSH_VEC_SIZE_BYTES + size_of::<PerpOpenOrder>() * usize::from(perp_oo_count))
}
}
#[test]
fn test_serialization_match() {
let mut account = MangoAccount::default();
account.group = Pubkey::new_unique();
account.owner = Pubkey::new_unique();
account.name = crate::util::fill_from_str("abcdef").unwrap();
account.delegate = Pubkey::new_unique();
account.account_num = 1;
account.bump = 2;
account.net_deposits = 3;
account.net_settled = 4;
account.tokens.resize(8, TokenPosition::default());
account.tokens[0].token_index = 5;
account.serum3.resize(8, Serum3Orders::default());
account.perps.resize(8, PerpPosition::default());
account.perps[0].market_index = 6;
account
.perp_open_orders
.resize(8, PerpOpenOrders::default());
let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap();
assert_eq!(
8 + account_bytes.len(),
MangoAccount::space(8, 8, 8, 8).unwrap()
);
let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap();
assert_eq!(account.group, account2.fixed.group);
assert_eq!(account.owner, account2.fixed.owner);
assert_eq!(account.name, account2.fixed.name);
assert_eq!(account.delegate, account2.fixed.delegate);
assert_eq!(account.account_num, account2.fixed.account_num);
assert_eq!(account.bump, account2.fixed.bump);
assert_eq!(account.net_deposits, account2.fixed.net_deposits);
assert_eq!(account.net_settled, account2.fixed.net_settled);
assert_eq!(
account.tokens[0].token_index,
account2.token_position_by_raw_index(0).token_index
);
assert_eq!(
account.serum3[0].open_orders,
account2.serum3_orders_by_raw_index(0).open_orders
);
assert_eq!(
account.perps[0].market_index,
account2.perp_position_by_raw_index(0).market_index
);
}
// Mango Account fixed part for easy zero copy deserialization
#[derive(Copy, Clone)]
#[repr(C)]
@ -230,14 +192,15 @@ pub struct MangoAccountFixed {
pub delegate: Pubkey,
pub account_num: u32,
being_liquidated: u8,
padding2: u8,
in_health_region: u8,
pub bump: u8,
pub padding: [u8; 1],
pub net_deposits: i64,
pub net_settled: i64,
pub reserved: [u8; 248],
pub health_region_begin_init_health: i64,
pub reserved: [u8; 240],
}
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 2 * 8 + 248);
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 3 * 8 + 240);
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
unsafe impl bytemuck::Pod for MangoAccountFixed {}
@ -255,18 +218,29 @@ impl MangoAccountFixed {
}
pub fn being_liquidated(&self) -> bool {
self.being_liquidated != 0
self.being_liquidated == 1
}
pub fn set_being_liquidated(&mut self, b: bool) {
self.being_liquidated = if b { 1 } else { 0 };
}
pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) {
pub fn is_in_health_region(&self) -> bool {
self.in_health_region == 1
}
pub fn set_in_health_region(&mut self, b: bool) {
self.in_health_region = if b { 1 } else { 0 };
}
pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) -> bool {
// This is used as threshold to flip flag instead of 0 because of dust issues
let one_native_usdc = I80F48::ONE;
if self.being_liquidated() && init_health > -one_native_usdc {
self.set_being_liquidated(false);
true
} else {
false
}
}
}
@ -374,7 +348,7 @@ impl MangoAccountDynamicHeader {
self.serum3_count,
self.perp_count,
) + BORSH_VEC_SIZE_BYTES
+ raw_index * size_of::<PerpOpenOrders>()
+ raw_index * size_of::<PerpOpenOrder>()
}
pub fn token_count(&self) -> usize {
@ -474,14 +448,13 @@ impl<
// get iter over all active TokenPositions
pub fn active_token_positions(&self) -> impl Iterator<Item = &TokenPosition> + '_ {
(0..self.header().token_count())
.map(|i| self.token_position_by_raw_index(i))
.filter(|token| token.is_active())
self.all_token_positions().filter(|token| token.is_active())
}
pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Option<&Serum3Orders> {
self.active_serum3_orders()
pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Result<&Serum3Orders> {
self.all_serum3_orders()
.find(|p| p.is_active_for_market(market_index))
.ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index))
}
pub fn serum3_orders_by_raw_index(&self, raw_index: usize) -> &Serum3Orders {
@ -493,14 +466,14 @@ impl<
}
pub fn active_serum3_orders(&self) -> impl Iterator<Item = &Serum3Orders> + '_ {
(0..self.header().serum3_count())
.map(|i| self.serum3_orders_by_raw_index(i))
self.all_serum3_orders()
.filter(|serum3_order| serum3_order.is_active())
}
pub fn perp_position(&self, market_index: PerpMarketIndex) -> Option<&PerpPosition> {
self.active_perp_positions()
pub fn perp_position(&self, market_index: PerpMarketIndex) -> Result<&PerpPosition> {
self.all_perp_positions()
.find(|p| p.is_active_for_market(market_index))
.ok_or_else(|| error_msg!("perp position for market index {} not found", market_index))
}
pub fn perp_position_by_raw_index(&self, raw_index: usize) -> &PerpPosition {
@ -512,22 +485,21 @@ impl<
}
pub fn active_perp_positions(&self) -> impl Iterator<Item = &PerpPosition> {
(0..self.header().perp_count())
.map(|i| self.perp_position_by_raw_index(i))
.filter(|p| p.is_active())
self.all_perp_positions().filter(|p| p.is_active())
}
pub fn perp_orders_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrders {
pub fn perp_order_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder {
get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index))
}
pub fn all_perp_orders(&self) -> impl Iterator<Item = &PerpOpenOrders> {
(0..self.header().perp_oo_count()).map(|i| self.perp_orders_by_raw_index(i))
pub fn all_perp_orders(&self) -> impl Iterator<Item = &PerpOpenOrder> {
(0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index(i))
}
pub fn perp_next_order_slot(&self) -> Option<usize> {
pub fn perp_next_order_slot(&self) -> Result<usize> {
self.all_perp_orders()
.position(|&oo| oo.order_market == FREE_ORDER_SLOT)
.ok_or_else(|| error_msg!("no free perp order index"))
}
pub fn perp_find_order_with_client_order_id(
@ -535,8 +507,7 @@ impl<
market_index: PerpMarketIndex,
client_order_id: u64,
) -> Option<(i128, Side)> {
for i in 0..self.header().perp_oo_count() {
let oo = self.perp_orders_by_raw_index(i);
for oo in self.all_perp_orders() {
if oo.order_market == market_index && oo.client_order_id == client_order_id {
return Some((oo.order_id, oo.order_side));
}
@ -549,8 +520,7 @@ impl<
market_index: PerpMarketIndex,
order_id: i128,
) -> Option<Side> {
for i in 0..self.header().perp_oo_count() {
let oo = self.perp_orders_by_raw_index(i);
for oo in self.all_perp_orders() {
if oo.order_market == market_index && oo.order_id == order_id {
return Some(oo.order_side);
}
@ -580,6 +550,9 @@ impl<
fn header_mut(&mut self) -> &mut MangoAccountDynamicHeader {
self.header.deref_or_borrow_mut()
}
fn fixed_mut(&mut self) -> &mut MangoAccountFixed {
self.fixed.deref_or_borrow_mut()
}
fn dynamic_mut(&mut self) -> &mut [u8] {
self.dynamic.deref_or_borrow_mut()
}
@ -669,7 +642,7 @@ impl<
&mut self,
market_index: Serum3MarketIndex,
) -> Result<&mut Serum3Orders> {
if self.serum3_orders(market_index).is_some() {
if self.serum3_orders(market_index).is_ok() {
return err!(MangoError::Serum3OpenOrdersExistAlready);
}
@ -679,9 +652,9 @@ impl<
market_index: market_index as Serum3MarketIndex,
..Serum3Orders::default()
};
return Ok(self.serum3_orders_mut_by_raw_index(raw_index));
Ok(self.serum3_orders_mut_by_raw_index(raw_index))
} else {
return err!(MangoError::NoFreeSerum3OpenOrdersIndex);
err!(MangoError::NoFreeSerum3OpenOrdersIndex)
}
}
@ -697,11 +670,13 @@ impl<
pub fn serum3_orders_mut(
&mut self,
market_index: Serum3MarketIndex,
) -> Option<&mut Serum3Orders> {
) -> Result<&mut Serum3Orders> {
let raw_index_opt = self
.active_serum3_orders()
.all_serum3_orders()
.position(|p| p.is_active_for_market(market_index));
raw_index_opt.map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index))
raw_index_opt
.map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index))
.ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index))
}
// get mut PerpPosition at raw_index
@ -710,7 +685,7 @@ impl<
get_helper_mut(self.dynamic_mut(), offset)
}
pub fn perp_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrders {
pub fn perp_order_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrder {
let offset = self.header().perp_oo_offset(raw_index);
get_helper_mut(self.dynamic_mut(), offset)
}
@ -720,7 +695,7 @@ impl<
perp_market_index: PerpMarketIndex,
) -> Result<(&mut PerpPosition, usize)> {
let mut raw_index_opt = self
.active_perp_positions()
.all_perp_positions()
.position(|p| p.is_active_for_market(perp_market_index));
if raw_index_opt.is_none() {
raw_index_opt = self.all_perp_positions().position(|p| !p.is_active());
@ -748,6 +723,7 @@ impl<
side: Side,
order: &LeafNode,
) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
let mut perp_account = self.ensure_perp_position(perp_market_index).unwrap().0;
match side {
Side::Bid => {
@ -759,7 +735,7 @@ impl<
};
let slot = order.owner_slot as usize;
let mut oo = self.perp_orders_mut_by_raw_index(slot);
let mut oo = self.perp_order_mut_by_raw_index(slot);
oo.order_market = perp_market_index;
oo.order_side = side;
oo.order_id = order.key;
@ -768,8 +744,9 @@ impl<
}
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
{
let oo = self.perp_orders_mut_by_raw_index(slot);
let oo = self.perp_order_mut_by_raw_index(slot);
require_neq!(oo.order_market, FREE_ORDER_SLOT);
let order_side = oo.order_side;
let perp_market_index = oo.order_market;
@ -787,7 +764,7 @@ impl<
}
// release space
let oo = self.perp_orders_mut_by_raw_index(slot);
let oo = self.perp_order_mut_by_raw_index(slot);
oo.order_market = FREE_ORDER_SLOT;
oo.order_side = Side::Bid;
oo.order_id = 0i128;
@ -801,6 +778,7 @@ impl<
perp_market: &mut PerpMarket,
fill: &FillEvent,
) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
let pa = self.ensure_perp_position(perp_market_index).unwrap().0;
pa.settle_funding(perp_market);
@ -840,6 +818,7 @@ impl<
perp_market: &mut PerpMarket,
fill: &FillEvent,
) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
let pa = self.ensure_perp_position(perp_market_index).unwrap().0;
pa.settle_funding(perp_market);
@ -854,6 +833,34 @@ impl<
Ok(())
}
pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result<I80F48> {
let pre_health = health_cache.health(HealthType::Init);
msg!("pre_health: {}", pre_health);
self.fixed_mut()
.maybe_recover_from_being_liquidated(pre_health);
require!(
!self.fixed().being_liquidated(),
MangoError::BeingLiquidated
);
Ok(pre_health)
}
pub fn check_health_post(
&mut self,
health_cache: &HealthCache,
pre_health: I80F48,
) -> Result<()> {
let post_health = health_cache.health(HealthType::Init);
msg!("post_health: {}", post_health);
require!(
post_health >= 0 || post_health > pre_health,
MangoError::HealthMustBePositiveOrIncrease
);
self.fixed_mut()
.maybe_recover_from_being_liquidated(post_health);
Ok(())
}
// writes length of tokens vec at appropriate offset so that borsh can infer the vector length
// length used is that present in the header
fn write_token_length(&mut self) {
@ -934,12 +941,12 @@ impl<
sol_memmove(
&mut dynamic[new_header.perp_oo_offset(0)],
&mut dynamic[old_header.perp_oo_offset(0)],
size_of::<PerpOpenOrders>() * old_header.perp_oo_count(),
size_of::<PerpOpenOrder>() * old_header.perp_oo_count(),
);
}
for i in old_header.perp_oo_count..new_perp_oo_count {
*get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) =
PerpOpenOrders::default();
PerpOpenOrder::default();
}
}
@ -1000,3 +1007,237 @@ impl<
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_account() -> MangoAccountValue {
let bytes = AnchorSerialize::try_to_vec(&MangoAccount::default_for_tests()).unwrap();
MangoAccountValue::from_bytes(&bytes).unwrap()
}
#[test]
fn test_serialization_match() {
let mut account = MangoAccount::default_for_tests();
account.group = Pubkey::new_unique();
account.owner = Pubkey::new_unique();
account.name = crate::util::fill_from_str("abcdef").unwrap();
account.delegate = Pubkey::new_unique();
account.account_num = 1;
account.being_liquidated = 2;
account.in_health_region = 3;
account.bump = 4;
account.net_deposits = 5;
account.net_settled = 6;
account.health_region_pre_init_health = 7;
account.tokens.resize(8, TokenPosition::default());
account.tokens[0].token_index = 8;
account.serum3.resize(8, Serum3Orders::default());
account.perps.resize(8, PerpPosition::default());
account.perps[0].market_index = 9;
account.perp_open_orders.resize(8, PerpOpenOrder::default());
let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap();
assert_eq!(
8 + account_bytes.len(),
MangoAccount::space(8, 8, 8, 8).unwrap()
);
let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap();
assert_eq!(account.group, account2.fixed.group);
assert_eq!(account.owner, account2.fixed.owner);
assert_eq!(account.name, account2.fixed.name);
assert_eq!(account.delegate, account2.fixed.delegate);
assert_eq!(account.account_num, account2.fixed.account_num);
assert_eq!(account.being_liquidated, account2.fixed.being_liquidated);
assert_eq!(account.in_health_region, account2.fixed.in_health_region);
assert_eq!(account.bump, account2.fixed.bump);
assert_eq!(account.net_deposits, account2.fixed.net_deposits);
assert_eq!(account.net_settled, account2.fixed.net_settled);
assert_eq!(
account.health_region_pre_init_health,
account2.fixed.health_region_begin_init_health
);
assert_eq!(
account.tokens[0].token_index,
account2.token_position_by_raw_index(0).token_index
);
assert_eq!(
account.serum3[0].open_orders,
account2.serum3_orders_by_raw_index(0).open_orders
);
assert_eq!(
account.perps[0].market_index,
account2.perp_position_by_raw_index(0).market_index
);
}
#[test]
fn test_token_positions() {
let mut account = make_test_account();
assert!(account.token_position(1).is_err());
assert!(account.token_position_and_raw_index(2).is_err());
assert!(account.token_position_mut(3).is_err());
assert_eq!(
account.token_position_by_raw_index(0).token_index,
TokenIndex::MAX
);
{
let (pos, raw, active) = account.ensure_token_position(1).unwrap();
assert_eq!(raw, 0);
assert_eq!(active, 0);
assert_eq!(pos.token_index, 1);
}
{
let (pos, raw, active) = account.ensure_token_position(7).unwrap();
assert_eq!(raw, 1);
assert_eq!(active, 1);
assert_eq!(pos.token_index, 7);
}
{
let (pos, raw, active) = account.ensure_token_position(42).unwrap();
assert_eq!(raw, 2);
assert_eq!(active, 2);
assert_eq!(pos.token_index, 42);
}
{
account.deactivate_token_position(1);
let (pos, raw, active) = account.ensure_token_position(42).unwrap();
assert_eq!(raw, 2);
assert_eq!(active, 1);
assert_eq!(pos.token_index, 42);
let (pos, raw, active) = account.ensure_token_position(8).unwrap();
assert_eq!(raw, 1);
assert_eq!(active, 1);
assert_eq!(pos.token_index, 8);
}
assert_eq!(account.active_token_positions().count(), 3);
account.deactivate_token_position(0);
assert_eq!(
account.token_position_by_raw_index(0).token_index,
TokenIndex::MAX
);
assert!(account.token_position(1).is_err());
assert!(account.token_position_mut(1).is_err());
assert!(account.token_position(8).is_ok());
assert!(account.token_position(42).is_ok());
assert_eq!(account.token_position_and_raw_index(42).unwrap().1, 2);
assert_eq!(account.active_token_positions().count(), 2);
{
let (pos, raw) = account.token_position_mut(42).unwrap();
assert_eq!(pos.token_index, 42);
assert_eq!(raw, 2);
}
{
let (pos, raw) = account.token_position_mut(8).unwrap();
assert_eq!(pos.token_index, 8);
assert_eq!(raw, 1);
}
}
#[test]
fn test_serum3_orders() {
let mut account = make_test_account();
assert!(account.serum3_orders(1).is_err());
assert!(account.serum3_orders_mut(3).is_err());
assert_eq!(
account.serum3_orders_by_raw_index(0).market_index,
Serum3MarketIndex::MAX
);
assert_eq!(account.create_serum3_orders(1).unwrap().market_index, 1);
assert_eq!(account.create_serum3_orders(7).unwrap().market_index, 7);
assert_eq!(account.create_serum3_orders(42).unwrap().market_index, 42);
assert!(account.create_serum3_orders(7).is_err());
assert_eq!(account.active_serum3_orders().count(), 3);
assert!(account.deactivate_serum3_orders(7).is_ok());
assert_eq!(
account.serum3_orders_by_raw_index(1).market_index,
Serum3MarketIndex::MAX
);
assert!(account.create_serum3_orders(8).is_ok());
assert_eq!(account.serum3_orders_by_raw_index(1).market_index, 8);
assert_eq!(account.active_serum3_orders().count(), 3);
assert!(account.deactivate_serum3_orders(1).is_ok());
assert!(account.serum3_orders(1).is_err());
assert!(account.serum3_orders_mut(1).is_err());
assert!(account.serum3_orders(8).is_ok());
assert!(account.serum3_orders(42).is_ok());
assert_eq!(account.active_serum3_orders().count(), 2);
assert_eq!(account.serum3_orders_mut(42).unwrap().market_index, 42);
assert_eq!(account.serum3_orders_mut(8).unwrap().market_index, 8);
assert!(account.serum3_orders_mut(7).is_err());
}
#[test]
fn test_perp_positions() {
let mut account = make_test_account();
assert!(account.perp_position(1).is_err());
//assert!(account.perp_position_mut(3).is_err());
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
PerpMarketIndex::MAX
);
{
let (pos, raw) = account.ensure_perp_position(1).unwrap();
assert_eq!(raw, 0);
assert_eq!(pos.market_index, 1);
}
{
let (pos, raw) = account.ensure_perp_position(7).unwrap();
assert_eq!(raw, 1);
assert_eq!(pos.market_index, 7);
}
{
let (pos, raw) = account.ensure_perp_position(42).unwrap();
assert_eq!(raw, 2);
assert_eq!(pos.market_index, 42);
}
{
account.deactivate_perp_position(1);
let (pos, raw) = account.ensure_perp_position(42).unwrap();
assert_eq!(raw, 2);
assert_eq!(pos.market_index, 42);
let (pos, raw) = account.ensure_perp_position(8).unwrap();
assert_eq!(raw, 1);
assert_eq!(pos.market_index, 8);
}
assert_eq!(account.active_perp_positions().count(), 3);
account.deactivate_perp_position(0);
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
PerpMarketIndex::MAX
);
assert!(account.perp_position(1).is_err());
//assert!(account.perp_position_mut(1).is_err());
assert!(account.perp_position(8).is_ok());
assert!(account.perp_position(42).is_ok());
assert_eq!(account.active_perp_positions().count(), 2);
/*{
let (pos, raw) = account.perp_position_mut(42).unwrap();
assert_eq!(pos.perp_index, 42);
assert_eq!(raw, 2);
}
{
let (pos, raw) = account.perp_position_mut(8).unwrap();
assert_eq!(pos.perp_index, 8);
assert_eq!(raw, 1);
}*/
}
}

View File

@ -92,12 +92,12 @@ impl TokenPosition {
pub struct Serum3Orders {
pub open_orders: Pubkey,
// tracks reserved funds in open orders account,
// used for bookkeeping of potentital loans which
// can be charged with loan origination fees
// e.g. serum3 settle funds ix
pub previous_native_coin_reserved: u64,
pub previous_native_pc_reserved: u64,
/// Tracks the amount of borrows that have flowed into the serum open orders account.
/// These borrows did not have the loan origination fee applied, and that may happen
/// later (in serum3_settle_funds) if we can guarantee that the funds were used.
/// In particular a place-on-book, cancel, settle should not cost fees.
pub base_borrows_without_fee: u64,
pub quote_borrows_without_fee: u64,
pub market_index: Serum3MarketIndex,
@ -138,8 +138,8 @@ impl Default for Serum3Orders {
quote_token_index: TokenIndex::MAX,
reserved: [0; 64],
padding: Default::default(),
previous_native_coin_reserved: 0,
previous_native_pc_reserved: 0,
base_borrows_without_fee: 0,
quote_borrows_without_fee: 0,
}
}
}
@ -321,7 +321,7 @@ impl PerpPosition {
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
pub struct PerpOpenOrders {
pub struct PerpOpenOrder {
pub order_side: Side, // TODO: storing enums isn't POD
pub padding1: [u8; 1],
pub order_market: PerpMarketIndex,
@ -331,7 +331,7 @@ pub struct PerpOpenOrders {
pub reserved: [u8; 64],
}
impl Default for PerpOpenOrders {
impl Default for PerpOpenOrder {
fn default() -> Self {
Self {
order_side: Side::Bid,
@ -345,11 +345,11 @@ impl Default for PerpOpenOrders {
}
}
unsafe impl bytemuck::Pod for PerpOpenOrders {}
unsafe impl bytemuck::Zeroable for PerpOpenOrders {}
unsafe impl bytemuck::Pod for PerpOpenOrder {}
unsafe impl bytemuck::Zeroable for PerpOpenOrder {}
const_assert_eq!(size_of::<PerpOpenOrders>(), 1 + 1 + 2 + 4 + 8 + 16 + 64);
const_assert_eq!(size_of::<PerpOpenOrders>() % 8, 0);
const_assert_eq!(size_of::<PerpOpenOrder>(), 1 + 1 + 2 + 4 + 8 + 16 + 64);
const_assert_eq!(size_of::<PerpOpenOrder>() % 8, 0);
#[macro_export]
macro_rules! account_seeds {

View File

@ -346,9 +346,7 @@ impl<'a> Book<'a> {
event_queue.push_back(cast(event)).unwrap();
}
let owner_slot = mango_account
.perp_next_order_slot()
.ok_or_else(|| error!(MangoError::SomeError))?;
let owner_slot = mango_account.perp_next_order_slot()?;
let new_order = LeafNode::new(
owner_slot as u8,
order_id,
@ -393,7 +391,7 @@ impl<'a> Book<'a> {
side_to_cancel_option: Option<Side>,
) -> Result<()> {
for i in 0..mango_account.header.perp_oo_count() {
let oo = mango_account.perp_orders_by_raw_index(i);
let oo = mango_account.perp_order_by_raw_index(i);
if oo.order_market == FREE_ORDER_SLOT
|| oo.order_market != perp_market.perp_market_index
{

View File

@ -101,7 +101,7 @@ mod tests {
let mut new_order =
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
let buffer = MangoAccount::default().try_to_vec().unwrap();
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
let quantity = 1;
@ -124,7 +124,7 @@ mod tests {
u8::MAX,
)
.unwrap();
account.perp_orders_by_raw_index(0).order_id
account.perp_order_by_raw_index(0).order_id
};
// insert bids until book side is full
@ -196,7 +196,7 @@ mod tests {
market.maker_fee = I80F48::from_num(-0.001f64);
market.taker_fee = I80F48::from_num(0.01f64);
let buffer = MangoAccount::default().try_to_vec().unwrap();
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap();
let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap();
@ -225,19 +225,19 @@ mod tests {
)
.unwrap();
assert_eq!(
maker.perp_orders_mut_by_raw_index(0).order_market,
maker.perp_order_mut_by_raw_index(0).order_market,
market.perp_market_index
);
assert_eq!(
maker.perp_orders_mut_by_raw_index(1).order_market,
maker.perp_order_mut_by_raw_index(1).order_market,
FREE_ORDER_SLOT
);
assert_ne!(maker.perp_orders_mut_by_raw_index(0).order_id, 0);
assert_eq!(maker.perp_orders_mut_by_raw_index(0).client_order_id, 42);
assert_eq!(maker.perp_orders_mut_by_raw_index(0).order_side, Side::Bid);
assert_ne!(maker.perp_order_mut_by_raw_index(0).order_id, 0);
assert_eq!(maker.perp_order_mut_by_raw_index(0).client_order_id, 42);
assert_eq!(maker.perp_order_mut_by_raw_index(0).order_side, Side::Bid);
assert!(bookside_contains_key(
&book.bids,
maker.perp_orders_mut_by_raw_index(0).order_id
maker.perp_order_mut_by_raw_index(0).order_id
));
assert!(bookside_contains_price(&book.bids, price));
assert_eq!(
@ -279,7 +279,7 @@ mod tests {
// the remainder of the maker order is still on the book
// (the maker account is unchanged: it was not even passed in)
let order =
bookside_leaf_by_key(&book.bids, maker.perp_orders_by_raw_index(0).order_id).unwrap();
bookside_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).order_id).unwrap();
assert_eq!(order.price(), price);
assert_eq!(order.quantity, bid_quantity - match_quantity);
@ -292,7 +292,7 @@ mod tests {
// the taker account is updated
assert_eq!(
taker.perp_orders_by_raw_index(0).order_market,
taker.perp_order_by_raw_index(0).order_market,
FREE_ORDER_SLOT
);
assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0);
@ -327,14 +327,14 @@ mod tests {
// simulate event queue processing
maker
.execute_perp_maker(market.perp_market_index, &mut market, &fill)
.execute_perp_maker(market.perp_market_index, &mut market, fill)
.unwrap();
taker
.execute_perp_taker(market.perp_market_index, &mut market, &fill)
.execute_perp_taker(market.perp_market_index, &mut market, fill)
.unwrap();
assert_eq!(market.open_interest, 2 * match_quantity);
assert_eq!(maker.perp_orders_by_raw_index(0).order_market, 0);
assert_eq!(maker.perp_order_by_raw_index(0).order_market, 0);
assert_eq!(
maker.perp_position_by_raw_index(0).bids_base_lots,
bid_quantity - match_quantity

View File

@ -37,5 +37,4 @@ pub fn format_zero_terminated_utf8_bytes(
.unwrap()
.trim_matches(char::from(0)),
)
.into()
}

View File

@ -319,6 +319,28 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
native.to_num::<f64>()
}
// Verifies that the "post_health: ..." log emitted by the previous instruction
// matches the init health of the account.
pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) {
let logs = solana.program_log();
let post_health_str = logs
.iter()
.find_map(|line| line.strip_prefix("post_health: "))
.unwrap();
let post_health = post_health_str.parse::<f64>().unwrap();
solana.advance_by_slots(1).await; // ugly, just to avoid sending the same tx next
send_tx(solana, ComputeAccountDataInstruction { account })
.await
.unwrap();
let health_data = solana
.program_log_events::<mango_v4::events::MangoAccountData>()
.pop()
.unwrap();
assert_eq!(health_data.init_health.to_num::<f64>(), post_health);
}
//
// a struct for each instruction along with its
// ClientInstruction impl
@ -502,6 +524,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
owner: self.owner.pubkey(),
bank: mint_info.banks[self.bank_index],
vault: mint_info.vaults[self.bank_index],
oracle: mint_info.oracle,
token_account: self.token_account,
token_program: Token::id(),
};
@ -569,6 +592,7 @@ impl ClientInstruction for TokenDepositInstruction {
account: self.account,
bank: mint_info.banks[self.bank_index],
vault: mint_info.vaults[self.bank_index],
oracle: mint_info.oracle,
token_account: self.token_account,
token_authority: self.token_authority.pubkey(),
token_program: Token::id(),
@ -812,9 +836,7 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> {
_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
token_index: self.token_index,
};
let instruction = Self::Instruction {};
let accounts = Self::Accounts {
admin: self.admin.pubkey(),
@ -1321,12 +1343,23 @@ impl<'keypair> ClientInstruction for Serum3RegisterMarketInstruction<'keypair> {
)
.0;
let index_reservation = Pubkey::find_program_address(
&[
b"Serum3Index".as_ref(),
self.group.as_ref(),
&self.market_index.to_le_bytes(),
],
&program_id,
)
.0;
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
serum_program: self.serum_program,
serum_market_external: self.serum_market_external,
serum_market,
index_reservation,
base_bank: self.base_bank,
quote_bank: self.quote_bank,
payer: self.payer.pubkey(),
@ -1567,14 +1600,17 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> {
)
.await;
let (payer_bank, payer_vault) = match self.side {
Serum3Side::Bid => (quote_info.first_bank(), quote_info.first_vault()),
Serum3Side::Ask => (base_info.first_bank(), base_info.first_vault()),
};
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
open_orders,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
payer_bank,
payer_vault,
serum_market: self.serum_market,
serum_program: serum_market.serum_program,
serum_market_external: serum_market.serum_market_external,
@ -2499,7 +2535,6 @@ impl ClientInstruction for TokenUpdateIndexAndRateInstruction {
pub struct ComputeAccountDataInstruction {
pub account: Pubkey,
pub health_type: HealthType,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for ComputeAccountDataInstruction {
@ -2541,3 +2576,91 @@ impl ClientInstruction for ComputeAccountDataInstruction {
vec![]
}
}
pub struct HealthRegionBeginInstruction {
pub account: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for HealthRegionBeginInstruction {
type Accounts = mango_v4::accounts::HealthRegionBegin;
type Instruction = mango_v4::instruction::HealthRegionBegin;
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 {};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
None,
)
.await;
let accounts = Self::Accounts {
instructions: solana_program::sysvar::instructions::id(),
account: self.account,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![]
}
}
pub struct HealthRegionEndInstruction {
pub account: Pubkey,
pub affected_bank: Option<Pubkey>,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for HealthRegionEndInstruction {
type Accounts = mango_v4::accounts::HealthRegionEnd;
type Instruction = mango_v4::instruction::HealthRegionEnd;
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 {};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
self.affected_bank,
false,
None,
)
.await;
let accounts = Self::Accounts {
account: self.account,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![]
}
}

View File

@ -5,7 +5,7 @@ use solana_sdk::signature::Keypair;
use super::mango_client::*;
use super::solana::SolanaCookie;
use super::{send_tx, MintCookie};
use super::{send_tx, ClonableKeypair, MintCookie, UserCookie};
pub struct GroupWithTokensConfig<'a> {
pub admin: &'a Keypair,
@ -13,6 +13,7 @@ pub struct GroupWithTokensConfig<'a> {
pub mints: &'a [MintCookie],
}
#[derive(Clone)]
pub struct Token {
pub index: u16,
pub mint: MintCookie,
@ -136,3 +137,48 @@ impl<'a> GroupWithTokensConfig<'a> {
}
}
}
pub async fn create_funded_account(
solana: &SolanaCookie,
group: Pubkey,
owner: &Keypair,
account_num: u32,
payer: &UserCookie,
mints: &[MintCookie],
amounts: u64,
bank_index: usize,
) -> Pubkey {
let account = send_tx(
solana,
AccountCreateInstruction {
account_num,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer: &payer.key,
},
)
.await
.unwrap()
.account;
for mint in mints {
send_tx(
solana,
TokenDepositInstruction {
amount: amounts,
account,
token_account: payer.token_accounts[mint.index],
token_authority: payer.key.clone(),
bank_index,
},
)
.await
.unwrap();
}
account
}

View File

@ -52,7 +52,7 @@ impl AddPacked for ProgramTest {
struct LoggerWrapper {
inner: env_logger::Logger,
program_log: Arc<RwLock<Vec<String>>>,
capture: Arc<RwLock<Vec<String>>>,
}
impl Log for LoggerWrapper {
@ -67,7 +67,9 @@ impl Log for LoggerWrapper {
{
let msg = record.args().to_string();
if let Some(data) = msg.strip_prefix("Program log: ") {
self.program_log.write().unwrap().push(data.into());
self.capture.write().unwrap().push(data.into());
} else if let Some(data) = msg.strip_prefix("Program data: ") {
self.capture.write().unwrap().push(data.into());
}
}
self.inner.log(record);
@ -85,14 +87,17 @@ pub struct MarginTradeCookie {
pub struct TestContextBuilder {
test: ProgramTest,
program_log_capture: Arc<RwLock<Vec<String>>>,
logger_capture: Arc<RwLock<Vec<String>>>,
mint0: Pubkey,
}
lazy_static::lazy_static! {
static ref LOGGER_CAPTURE: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
static ref LOGGER_LOCK: Arc<RwLock<()>> = Arc::new(RwLock::new(()));
}
impl TestContextBuilder {
pub fn new() -> Self {
let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry));
// We need to intercept logs to capture program log output
let log_filter = "solana_rbpf=trace,\
solana_runtime::message_processor=debug,\
@ -102,18 +107,19 @@ impl TestContextBuilder {
env_logger::Builder::from_env(env_logger::Env::new().default_filter_or(log_filter))
.format_timestamp_nanos()
.build();
let program_log_capture = Arc::new(RwLock::new(vec![]));
let _ = log::set_boxed_logger(Box::new(LoggerWrapper {
inner: env_logger,
program_log: program_log_capture.clone(),
capture: LOGGER_CAPTURE.clone(),
}));
let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry));
// intentionally set to as tight as possible, to catch potential problems early
test.set_compute_max_units(87000);
test.set_compute_max_units(75000);
Self {
test,
program_log_capture,
logger_capture: LOGGER_CAPTURE.clone(),
mint0: Pubkey::new_unique(),
}
}
@ -277,7 +283,9 @@ impl TestContextBuilder {
let solana = Arc::new(SolanaCookie {
context: RefCell::new(context),
rent,
program_log: self.program_log_capture.clone(),
logger_capture: self.logger_capture.clone(),
logger_lock: LOGGER_LOCK.clone(),
last_transaction_log: RefCell::new(vec![]),
});
solana

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::{mem, sync::Arc};
use bytemuck::from_bytes;
@ -41,7 +43,6 @@ pub struct SerumCookie {
}
impl SerumCookie {
#[allow(dead_code)]
pub fn create_dex_account(&self, unpadded_len: usize) -> (Keypair, Instruction) {
let serum_program_id = self.program_id;
let key = Keypair::new();
@ -57,7 +58,6 @@ impl SerumCookie {
return (key, create_account_instr);
}
#[allow(dead_code)]
fn gen_listing_params(
&self,
_coin_mint: &Pubkey,
@ -94,7 +94,6 @@ impl SerumCookie {
return (info, instructions);
}
#[allow(dead_code)]
pub async fn list_spot_market(
&self,
coin_mint: &MintCookie,
@ -186,20 +185,19 @@ impl SerumCookie {
}
}
#[allow(dead_code)]
pub async fn consume_spot_events(
&self,
spot_market_cookie: &SpotMarketCookie,
open_orders: Pubkey,
open_orders: &[Pubkey],
) {
let instructions = [serum_dex::instruction::consume_events(
&self.program_id,
vec![&open_orders],
open_orders.iter().collect(),
&spot_market_cookie.market,
&spot_market_cookie.event_q,
&spot_market_cookie.coin_fee_account,
&spot_market_cookie.pc_fee_account,
5,
10,
)
.unwrap()];
self.solana
@ -208,13 +206,11 @@ impl SerumCookie {
.unwrap();
}
#[allow(dead_code)]
fn strip_dex_padding(data: &[u8]) -> &[u8] {
assert!(data.len() >= 12);
&data[5..data.len() - 7]
}
#[allow(dead_code)]
pub async fn load_open_orders(&self, open_orders: Pubkey) -> serum_dex::state::OpenOrders {
let data = self.solana.get_account_data(open_orders).await.unwrap();
let slice = Self::strip_dex_padding(&data);

View File

@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::cell::RefCell;
use std::sync::{Arc, RwLock};
@ -17,17 +19,26 @@ use spl_token::*;
pub struct SolanaCookie {
pub context: RefCell<ProgramTestContext>,
pub rent: Rent,
pub program_log: Arc<RwLock<Vec<String>>>,
pub logger_capture: Arc<RwLock<Vec<String>>>,
pub logger_lock: Arc<RwLock<()>>,
pub last_transaction_log: RefCell<Vec<String>>,
}
impl SolanaCookie {
#[allow(dead_code)]
pub async fn process_transaction(
&self,
instructions: &[Instruction],
signers: Option<&[&Keypair]>,
) -> Result<(), BanksClientError> {
self.program_log.write().unwrap().clear();
// The locking in this function is convoluted:
// We capture the program log output by overriding the global logger and capturing
// messages there. This logger is potentially shared among multiple tests that run
// concurrently.
// To allow each independent SolanaCookie to capture only the logs from the transaction
// passed to process_transaction, wo globally hold the "program_log_lock" for the
// duration that the tx needs to process. So only a single one can run at a time.
let tx_log_lock = Arc::new(self.logger_lock.write().unwrap());
self.logger_capture.write().unwrap().clear();
let mut context = self.context.borrow_mut();
@ -45,13 +56,19 @@ impl SolanaCookie {
transaction.sign(&all_signers, context.last_blockhash);
context
let result = context
.banks_client
.process_transaction_with_commitment(
transaction,
solana_sdk::commitment_config::CommitmentLevel::Processed,
)
.await
.await;
*self.last_transaction_log.borrow_mut() = self.logger_capture.read().unwrap().clone();
drop(tx_log_lock);
result
}
pub async fn get_clock(&self) -> solana_program::clock::Clock {
@ -63,7 +80,6 @@ impl SolanaCookie {
.unwrap()
}
#[allow(dead_code)]
pub async fn advance_by_slots(&self, slots: u64) {
let clock = self.get_clock().await;
self.context
@ -72,8 +88,6 @@ impl SolanaCookie {
.unwrap();
}
#[allow(dead_code)]
pub async fn advance_clock(&self) {
let mut clock = self.get_clock().await;
let old_ts = clock.unix_timestamp;
@ -133,7 +147,6 @@ impl SolanaCookie {
key.pubkey()
}
#[allow(dead_code)]
pub async fn create_token_account(&self, owner: &Pubkey, mint: Pubkey) -> Pubkey {
let keypair = Keypair::new();
let rent = self.rent.minimum_balance(spl_token::state::Account::LEN);
@ -162,7 +175,6 @@ impl SolanaCookie {
}
// Note: Only one table can be created per authority per slot!
#[allow(dead_code)]
pub async fn create_address_lookup_table(
&self,
authority: &Keypair,
@ -180,7 +192,6 @@ impl SolanaCookie {
alt_address
}
#[allow(dead_code)]
pub async fn get_account_data(&self, address: Pubkey) -> Option<Vec<u8>> {
Some(
self.context
@ -194,7 +205,6 @@ impl SolanaCookie {
)
}
#[allow(dead_code)]
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
self.context
.borrow_mut()
@ -209,18 +219,30 @@ impl SolanaCookie {
AccountDeserialize::try_deserialize(&mut data_slice).ok()
}
#[allow(dead_code)]
pub async fn get_account<T: AccountDeserialize>(&self, address: Pubkey) -> T {
self.get_account_opt(address).await.unwrap()
}
#[allow(dead_code)]
pub async fn token_account_balance(&self, address: Pubkey) -> u64 {
self.get_account::<TokenAccount>(address).await.amount
}
#[allow(dead_code)]
pub fn program_log(&self) -> Vec<String> {
self.program_log.read().unwrap().clone()
self.last_transaction_log.borrow().clone()
}
pub fn program_log_events<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
&self,
) -> Vec<T> {
self.program_log()
.iter()
.filter_map(|data| {
let bytes = base64::decode(data).ok()?;
if bytes[0..8] != T::discriminator() {
return None;
}
T::try_from_slice(&bytes[8..]).ok()
})
.collect()
}
}

View File

@ -1,15 +1,15 @@
#![allow(dead_code)]
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] {
[acc_pk.as_ref(), bytes_of(nonce)]
}
#[allow(dead_code)]
pub fn gen_signer_key(
nonce: u64,
acc_pk: &Pubkey,
@ -19,7 +19,6 @@ pub fn gen_signer_key(
Ok(Pubkey::create_program_address(&seeds, program_id)?)
}
#[allow(dead_code)]
pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pubkey, u64) {
for i in 0..=u64::MAX_VALUE {
if let Ok(pk) = gen_signer_key(i, acc_pk, program_id) {
@ -29,7 +28,6 @@ pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pub
panic!("Could not generate signer key");
}
#[allow(dead_code)]
pub fn clone_keypair(keypair: &Keypair) -> Keypair {
Keypair::from_base58_string(&keypair.to_base58_string())
}

View File

@ -10,6 +10,8 @@ use solana_sdk::{
use mango_v4::state::*;
use program_test::*;
use mango_setup::*;
mod program_test;
#[tokio::test]
@ -27,7 +29,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
// SETUP: Create a group and an account to fill the vaults
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -40,37 +42,18 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
let collateral_token2 = &tokens[3];
// deposit some funds, to the vaults aren't empty
let vault_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 2,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
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();
}
let vault_account = create_funded_account(
&solana,
group,
owner,
2,
&context.users[1],
mints,
vault_amount,
1,
)
.await;
// also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss
send_tx(

View File

@ -111,15 +111,14 @@ async fn test_basic() -> Result<(), TransportError> {
//
// TEST: Compute the account health
//
send_tx(
solana,
ComputeAccountDataInstruction {
account,
health_type: HealthType::Init,
},
)
.await
.unwrap();
send_tx(solana, ComputeAccountDataInstruction { account })
.await
.unwrap();
let health_data = solana
.program_log_events::<mango_v4::events::MangoAccountData>()
.pop()
.unwrap();
assert_eq!(health_data.init_health.to_num::<i64>(), 60);
//
// TEST: Withdraw funds
@ -143,6 +142,8 @@ async fn test_basic() -> Result<(), TransportError> {
.await
.unwrap();
check_prev_instruction_post_health(&solana, account).await;
assert_eq!(solana.token_account_balance(vault).await, withdraw_amount);
assert_eq!(
solana.token_account_balance(payer_mint0_account).await,

View File

@ -6,6 +6,8 @@ use solana_sdk::{signature::Keypair, signature::Signer, transport::TransportErro
use mango_v4::state::*;
use program_test::*;
use mango_setup::*;
mod program_test;
#[tokio::test]
@ -24,7 +26,7 @@ async fn test_delegate() -> Result<(), TransportError> {
// SETUP: Create a group, register a token (mint0), create an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -33,36 +35,8 @@ async fn test_delegate() -> Result<(), TransportError> {
.await;
let bank = tokens[0].bank;
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
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();
let account =
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 100, 0).await;
//
// TEST: Edit account - Set delegate

View File

@ -7,6 +7,8 @@ use solana_sdk::{signature::Keypair, transport::TransportError};
use program_test::*;
use mango_setup::*;
mod program_test;
// Try to reach compute limits in health checks by having many different tokens in an account
@ -19,13 +21,12 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..10];
let payer_mint_accounts = &context.users[1].token_accounts[0..mints.len()];
//
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -33,43 +34,8 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
.create(solana)
.await;
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// TEST: Deposit user funds for all the mints
// each deposit will end with a health check
//
for &token_account in payer_mint_accounts {
let deposit_amount = 1000;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account,
token_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
// each deposit ends with a health check
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await;
// TODO: actual explicit CU comparisons.
// On 2022-5-25 the final deposit costs 36905 CU and each new token increases it by roughly 1600 CU
@ -216,36 +182,17 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
.create(solana)
.await;
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints[..1],
1000,
0,
)
.await
.unwrap()
.account;
// Give the account some quote currency
send_tx(
solana,
TokenDepositInstruction {
amount: 1000,
account,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
.await;
//
// SETUP: Create perp markets

View File

@ -0,0 +1,151 @@
#![cfg(feature = "test-bpf")]
use solana_program_test::*;
use solana_sdk::{signature::Keypair, transport::TransportError};
use mango_v4::state::MangoAccount;
use program_test::*;
use mango_setup::*;
mod program_test;
#[tokio::test]
async fn test_health_wrap() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint_accounts = &context.users[1].token_accounts;
//
// SETUP: Create a group, account, register a token (mint0)
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
// SETUP: Create an account with deposits, so the second account can borrow more than it has
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await;
// SETUP: Make a second account
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 8,
serum3_count: 0,
perp_count: 0,
perp_oo_count: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
// SETUP: deposit something, so only one new token position needs to be created
// simply because the test code can't deal with two affected banks right now
send_tx(
solana,
TokenDepositInstruction {
amount: 1,
account,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
let send_test_tx = |repay_amount| {
let tokens = tokens.clone();
async move {
let mut tx = ClientTransaction::new(solana);
tx.add_instruction(HealthRegionBeginInstruction { account })
.await;
tx.add_instruction(TokenWithdrawInstruction {
amount: 1000, // more than the 1 token that's on the account
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
})
.await;
tx.add_instruction(TokenDepositInstruction {
amount: repay_amount,
account,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
})
.await;
tx.add_instruction(HealthRegionEndInstruction {
account,
affected_bank: Some(tokens[1].bank),
})
.await;
tx.send().await
}
};
//
// TEST: Borrow a lot of token0 without collateral, but repay too little
//
{
send_test_tx(1000).await.unwrap_err();
let logs = solana.program_log();
// reaches the End instruction
assert!(logs
.iter()
.any(|line| line.contains("Instruction: HealthRegionEnd")));
// errors due to health
assert!(logs
.iter()
.any(|line| line.contains("Error Code: HealthMustBePositiveOrIncrease")));
}
//
// TEST: Borrow a lot of token0 without collateral, and repay in token1 in the same tx
//
{
let start_payer_mint0 = solana.token_account_balance(payer_mint_accounts[0]).await;
let start_payer_mint1 = solana.token_account_balance(payer_mint_accounts[1]).await;
send_test_tx(3000).await.unwrap();
assert_eq!(
solana.token_account_balance(payer_mint_accounts[0]).await - start_payer_mint0,
1000
);
assert_eq!(
start_payer_mint1 - solana.token_account_balance(payer_mint_accounts[1]).await,
3000
);
assert_eq!(
account_position(solana, account, tokens[0].bank).await,
-999
);
assert_eq!(
account_position(solana, account, tokens[1].bank).await,
3000
);
let account_data: MangoAccount = solana.get_account(account).await;
assert_eq!(account_data.in_health_region, 0);
}
Ok(())
}

View File

@ -7,11 +7,15 @@ use solana_sdk::{signature::Keypair, transport::TransportError};
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use program_test::*;
use mango_setup::*;
mod program_test;
#[tokio::test]
async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
let context = TestContext::new().await;
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
@ -24,7 +28,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 GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -35,36 +39,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
let quote_token = &tokens[1];
// deposit some funds, to the vaults aren't empty
let vault_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 2,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
for &token_account in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: 10000,
account: vault_account,
token_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await;
//
// SETUP: Create serum market
@ -94,36 +69,18 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
//
// SETUP: Make an account and deposit some quote
//
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let deposit_amount = 1000;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
let account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[1..2],
deposit_amount,
0,
)
.await
.unwrap();
.await;
//
// SETUP: Create an open orders account and an order

View File

@ -6,6 +6,8 @@ use solana_sdk::signature::Signer;
use program_test::*;
use mango_setup::*;
mod program_test;
// This is an unspecific happy-case test that just runs a few instructions to check
@ -21,7 +23,6 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint0_account = context.users[1].token_accounts[0];
let payer_mint1_account = context.users[1].token_accounts[1];
let loan_origination_fee = 0.0005;
// higher resolution that the loan_origination_fee for one token
@ -31,7 +32,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -45,48 +46,17 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
// provide some funds for tokens, so the test user can borrow
//
let provided_amount = 1000;
let provider_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
provided_amount,
0,
)
.await
.unwrap()
.account;
send_tx(
solana,
TokenDepositInstruction {
amount: provided_amount,
account: provider_account,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: provided_amount,
account: provider_account,
token_account: payer_mint1_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
.await;
//
// create thes test user account

View File

@ -7,6 +7,8 @@ use program_test::*;
use solana_program_test::*;
use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError};
use mango_setup::*;
mod program_test;
#[tokio::test]
@ -18,13 +20,12 @@ async fn test_perp() -> Result<(), TransportError> {
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..=2];
//
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -32,102 +33,29 @@ async fn test_perp() -> Result<(), TransportError> {
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await
.unwrap()
.account;
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await
.unwrap()
.account;
//
// SETUP: Deposit user funds
//
{
let deposit_amount = 1000;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
{
let deposit_amount = 1000;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
.await;
//
// TEST: Create a perp market
@ -204,6 +132,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
let order_id_to_cancel = solana
.get_account::<MangoAccount>(account_0)
@ -250,6 +179,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
@ -291,6 +221,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
@ -311,6 +242,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
@ -331,6 +263,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
@ -371,6 +304,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
@ -392,6 +326,7 @@ async fn test_perp() -> Result<(), TransportError> {
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,

View File

@ -6,6 +6,8 @@ use solana_sdk::signature::Keypair;
use program_test::*;
use crate::mango_setup::*;
mod program_test;
// Check opening and closing positions
@ -50,43 +52,18 @@ async fn test_position_lifetime() -> Result<()> {
.unwrap()
.account;
let funding_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
let funding_amount = 1000000;
create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
funding_amount,
0,
)
.await
.unwrap()
.account;
//
// SETUP: Put some tokens into the funding account to allow borrowing
//
{
let funding_amount = 1000000;
for &payer_token in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: funding_amount,
account: funding_account,
token_account: payer_token,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
}
.await;
//
// TEST: Deposit and withdraw tokens for all mints

View File

@ -1,29 +1,169 @@
#![cfg(feature = "test-bpf")]
#![allow(dead_code)]
use solana_program_test::*;
use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError};
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::instructions::{
OpenOrdersSlim, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
};
use mango_v4::state::Serum3Orders;
use program_test::*;
mod program_test;
use mango_setup::*;
use std::sync::Arc;
struct SerumOrderPlacer {
solana: Arc<SolanaCookie>,
serum: Arc<SerumCookie>,
account: Pubkey,
owner: Keypair,
serum_market: Pubkey,
open_orders: Pubkey,
next_client_order_id: u64,
}
impl SerumOrderPlacer {
fn inc_client_order_id(&mut self) -> u64 {
let id = self.next_client_order_id;
self.next_client_order_id += 1;
id
}
async fn find_order_id_for_client_order_id(&self, client_order_id: u64) -> Option<(u128, u64)> {
let open_orders = self.serum.load_open_orders(self.open_orders).await;
for i in 0..128 {
if open_orders.free_slot_bits & (1u128 << i) != 0 {
continue;
}
if open_orders.client_order_ids[i] == client_order_id {
return Some((open_orders.orders[i], client_order_id));
}
}
None
}
async fn bid(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
let client_order_id = self.inc_client_order_id();
send_tx(
&self.solana,
Serum3PlaceOrderInstruction {
side: Serum3Side::Bid,
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
max_base_qty: max_base / 100, // in base lot (100)
max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64,
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
order_type: Serum3OrderType::Limit,
client_order_id,
limit: 10,
account: self.account,
owner: &self.owner,
serum_market: self.serum_market,
},
)
.await
.unwrap();
self.find_order_id_for_client_order_id(client_order_id)
.await
}
async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
let client_order_id = self.inc_client_order_id();
send_tx(
&self.solana,
Serum3PlaceOrderInstruction {
side: Serum3Side::Ask,
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
max_base_qty: max_base / 100, // in base lot (100)
max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64,
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
order_type: Serum3OrderType::Limit,
client_order_id,
limit: 10,
account: self.account,
owner: &self.owner,
serum_market: self.serum_market,
},
)
.await
.unwrap();
self.find_order_id_for_client_order_id(client_order_id)
.await
}
async fn cancel(&self, order_id: u128) {
let side = {
let open_orders = self.serum.load_open_orders(self.open_orders).await;
let orders = open_orders.orders;
let idx = orders.iter().position(|&v| v == order_id).unwrap();
if open_orders.is_bid_bits & (1u128 << idx) == 0 {
Serum3Side::Ask
} else {
Serum3Side::Bid
}
};
send_tx(
&self.solana,
Serum3CancelOrderInstruction {
side,
order_id,
account: self.account,
owner: &self.owner,
serum_market: self.serum_market,
},
)
.await
.unwrap();
}
async fn settle(&self) {
// to avoid multiple settles looking like the same tx
self.solana.advance_by_slots(1).await;
send_tx(
&self.solana,
Serum3SettleFundsInstruction {
account: self.account,
owner: &self.owner,
serum_market: self.serum_market,
},
)
.await
.unwrap();
}
async fn mango_serum_orders(&self) -> Serum3Orders {
let account_data = get_mango_account(&self.solana, self.account).await;
let orders = account_data
.all_serum3_orders()
.find(|s| s.open_orders == self.open_orders)
.unwrap();
orders.clone()
}
async fn open_orders(&self) -> OpenOrdersSlim {
OpenOrdersSlim::from_oo(&self.serum.load_open_orders(self.open_orders).await)
}
}
#[tokio::test]
async fn test_serum() -> Result<(), TransportError> {
let context = TestContext::new().await;
async fn test_serum_basics() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..=2];
//
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -33,23 +173,6 @@ async fn test_serum() -> Result<(), TransportError> {
let base_token = &tokens[0];
let quote_token = &tokens[1];
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// SETUP: Create serum market
//
@ -58,39 +181,6 @@ async fn test_serum() -> Result<(), TransportError> {
.list_spot_market(&base_token.mint, &quote_token.mint)
.await;
//
// SETUP: Deposit user funds
//
{
let deposit_amount = 1000;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
//
// TEST: Register a serum market
//
@ -111,6 +201,22 @@ async fn test_serum() -> Result<(), TransportError> {
.unwrap()
.serum_market;
//
// SETUP: Create account
//
let deposit_amount = 1000;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: Create an open orders account
//
@ -136,89 +242,74 @@ async fn test_serum() -> Result<(), TransportError> {
[(open_orders, 0)]
);
let mut order_placer = SerumOrderPlacer {
solana: solana.clone(),
serum: context.serum.clone(),
account,
owner: owner.clone(),
serum_market,
open_orders,
next_client_order_id: 0,
};
//
// TEST: Place an order
//
send_tx(
solana,
Serum3PlaceOrderInstruction {
side: Serum3Side::Bid,
limit_price: 10, // in quote_lot (10) per base lot (100)
max_base_qty: 1, // in base lot (100)
max_native_quote_qty_including_fees: 100,
self_trade_behavior: Serum3SelfTradeBehavior::DecrementTake,
order_type: Serum3OrderType::Limit,
client_order_id: 0,
limit: 10,
account,
owner,
serum_market,
},
)
.await
.unwrap();
let (order_id, _) = order_placer.bid(1.0, 100).await.unwrap();
check_prev_instruction_post_health(&solana, account).await;
let native0 = account_position(solana, account, base_token.bank).await;
let native1 = account_position(solana, account, quote_token.bank).await;
assert_eq!(native0, 1000);
assert_eq!(native1, 900);
// get the order id
let open_orders_bytes = solana.get_account_data(open_orders).await.unwrap();
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes(
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()],
);
let order_id = open_orders_data.orders[0];
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 1);
assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 1);
assert_eq!(account_data.token_position_by_raw_index(2).in_use_count, 0);
let serum_orders = account_data.serum3_orders_by_raw_index(0);
assert_eq!(serum_orders.base_borrows_without_fee, 0);
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
assert!(order_id != 0);
//
// TEST: Cancel the order
//
send_tx(
solana,
Serum3CancelOrderInstruction {
side: Serum3Side::Bid,
order_id,
account,
owner,
serum_market,
},
)
.await
.unwrap();
order_placer.cancel(order_id).await;
//
// TEST: Settle, moving the freed up funds back
//
send_tx(
solana,
Serum3SettleFundsInstruction {
account,
owner,
serum_market,
},
)
.await
.unwrap();
order_placer.settle().await;
let native0 = account_position(solana, account, base_token.bank).await;
let native1 = account_position(solana, account, quote_token.bank).await;
assert_eq!(native0, 1000);
assert_eq!(native1, 1000);
// Process events such that the OutEvent deactivates the closed order on open_orders
context
.serum
.consume_spot_events(&serum_market_cookie, &[open_orders])
.await;
// close oo account
// TODO: custom program error: 0x2a TooManyOpenOrders https://github.com/project-serum/serum-dex/blob/master/dex/src/error.rs#L88
// send_tx(
// solana,
// Serum3CloseOpenOrdersInstruction {
// account,
// serum_market,
// owner,
// sol_destination: payer.pubkey(),
// },
// )
// .await
// .unwrap();
send_tx(
solana,
Serum3CloseOpenOrdersInstruction {
account,
serum_market,
owner,
sol_destination: payer.pubkey(),
},
)
.await
.unwrap();
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 0);
assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 0);
// deregister serum3 market
send_tx(
@ -235,3 +326,257 @@ async fn test_serum() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
let context = test_builder.start_default().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..3];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let base_token = &tokens[0];
let base_bank = base_token.bank;
let quote_token = &tokens[1];
let quote_bank = quote_token.bank;
//
// SETUP: Create serum market
//
let serum_market_cookie = context
.serum
.list_spot_market(&base_token.mint, &quote_token.mint)
.await;
//
// SETUP: Register a serum market
//
let serum_market = send_tx(
solana,
Serum3RegisterMarketInstruction {
group,
admin,
serum_program: context.serum.program_id,
serum_market_external: serum_market_cookie.market,
market_index: 0,
base_bank: base_token.bank,
quote_bank: quote_token.bank,
payer,
},
)
.await
.unwrap()
.serum_market;
//
// SETUP: Create accounts
//
let deposit_amount = 180000;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account2 = create_funded_account(
&solana,
group,
owner,
2,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
// to have enough funds in the vaults
create_funded_account(
&solana,
group,
owner,
3,
&context.users[1],
mints,
10000000,
0,
)
.await;
let open_orders = send_tx(
solana,
Serum3CreateOpenOrdersInstruction {
account,
serum_market,
owner,
payer,
},
)
.await
.unwrap()
.open_orders;
let open_orders2 = send_tx(
solana,
Serum3CreateOpenOrdersInstruction {
account: account2,
serum_market,
owner,
payer,
},
)
.await
.unwrap()
.open_orders;
let mut order_placer = SerumOrderPlacer {
solana: solana.clone(),
serum: context.serum.clone(),
account,
owner: owner.clone(),
serum_market,
open_orders,
next_client_order_id: 0,
};
let mut order_placer2 = SerumOrderPlacer {
solana: solana.clone(),
serum: context.serum.clone(),
account: account2,
owner: owner.clone(),
serum_market,
open_orders: open_orders2,
next_client_order_id: 100000,
};
//
// TEST: Placing and canceling an order does not take loan origination fees even if borrows are needed
//
{
let (bid_order_id, _) = order_placer.bid(1.0, 200000).await.unwrap();
let (ask_order_id, _) = order_placer.ask(2.0, 200000).await.unwrap();
let o = order_placer.mango_serum_orders().await;
assert_eq!(o.base_borrows_without_fee, 19999); // rounded
assert_eq!(o.quote_borrows_without_fee, 19999);
order_placer.cancel(bid_order_id).await;
order_placer.cancel(ask_order_id).await;
let o = order_placer.mango_serum_orders().await;
assert_eq!(o.base_borrows_without_fee, 19999); // unchanged
assert_eq!(o.quote_borrows_without_fee, 19999);
// placing new, slightly larger orders increases the borrow_without_fee amount only by a small amount
let (bid_order_id, _) = order_placer.bid(1.0, 210000).await.unwrap();
let (ask_order_id, _) = order_placer.ask(2.0, 300000).await.unwrap();
let o = order_placer.mango_serum_orders().await;
assert_eq!(o.base_borrows_without_fee, 119998); // rounded
assert_eq!(o.quote_borrows_without_fee, 29998);
order_placer.cancel(bid_order_id).await;
order_placer.cancel(ask_order_id).await;
// returns all the funds
order_placer.settle().await;
let o = order_placer.mango_serum_orders().await;
assert_eq!(o.base_borrows_without_fee, 0);
assert_eq!(o.quote_borrows_without_fee, 0);
assert_eq!(
account_position(solana, account, quote_bank).await,
deposit_amount as i64
);
assert_eq!(
account_position(solana, account, base_bank).await,
deposit_amount as i64
);
// consume all the out events from the cancels
context
.serum
.consume_spot_events(&serum_market_cookie, &[open_orders])
.await;
}
let without_serum_taker_fee = |amount: i64| (amount as f64 * (1.0 - 0.0022)).trunc() as i64;
let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0003).round() as i64;
let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64;
//
// TEST: Order execution and settling charges borrow fee
//
{
let deposit_amount = deposit_amount as i64;
let bid_amount = 200000;
let ask_amount = 210000;
let fill_amount = 200000;
// account2 has an order on the book
order_placer2.bid(1.0, bid_amount as u64).await.unwrap();
// account takes
order_placer.ask(1.0, ask_amount as u64).await.unwrap();
order_placer.settle().await;
let o = order_placer.mango_serum_orders().await;
// parts of the order ended up on the book an may cause loan origination fees later
assert_eq!(
o.base_borrows_without_fee,
(ask_amount - fill_amount) as u64
);
assert_eq!(o.quote_borrows_without_fee, 0);
assert_eq!(
account_position(solana, account, quote_bank).await,
deposit_amount + without_serum_taker_fee(fill_amount)
);
assert_eq!(
account_position(solana, account, base_bank).await,
deposit_amount - ask_amount - loan_origination_fee(fill_amount - deposit_amount)
);
// check account2 balances too
context
.serum
.consume_spot_events(&serum_market_cookie, &[open_orders, open_orders2])
.await;
order_placer2.settle().await;
let o = order_placer2.mango_serum_orders().await;
assert_eq!(o.base_borrows_without_fee, 0);
assert_eq!(o.quote_borrows_without_fee, 0);
assert_eq!(
account_position(solana, account2, base_bank).await,
deposit_amount + fill_amount
);
assert_eq!(
account_position(solana, account2, quote_bank).await,
deposit_amount - fill_amount - loan_origination_fee(fill_amount - deposit_amount)
+ serum_maker_rebate(fill_amount)
);
}
Ok(())
}

View File

@ -4,6 +4,7 @@ use mango_v4::state::*;
use solana_program_test::*;
use solana_sdk::{signature::Keypair, transport::TransportError};
use mango_setup::*;
use program_test::*;
mod program_test;
@ -17,13 +18,12 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..2];
//
// SETUP: Create a group and an account to fill the vaults
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
@ -32,66 +32,18 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
.await;
// deposit some funds, to the vaults aren't empty
let deposit_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await;
let withdraw_account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[1..2],
100000,
0,
)
.await
.unwrap()
.account;
for &token_account in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: 10000,
account: deposit_account,
token_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
let withdraw_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
send_tx(
solana,
TokenDepositInstruction {
amount: 100000,
account: withdraw_account,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
.await;
send_tx(
solana,

View File

@ -197,16 +197,16 @@ export class I80F48 {
return this.data.cmp(x.getData());
}
neg(): I80F48 {
return this.mul(NEG_ONE_I80F48);
return this.mul(_NEG_ONE_I80F48);
}
isPos(): boolean {
return this.gt(ZERO_I80F48);
return this.gt(_ZERO_I80F48);
}
isNeg(): boolean {
return this.data.isNeg();
}
isZero(): boolean {
return this.eq(ZERO_I80F48);
return this.eq(_ZERO_I80F48);
}
min(x: I80F48): I80F48 {
return this.lte(x) ? this : x;
@ -224,10 +224,22 @@ export class I80F48 {
}
/** @internal */
export const ONE_I80F48 = I80F48.fromString('1');
const _ZERO_I80F48 = I80F48.fromNumber(0);
/** @internal */
export const ZERO_I80F48 = I80F48.fromString('0');
/** @internal */
export const NEG_ONE_I80F48 = I80F48.fromString('-1');
export const HUNDRED_I80F48 = I80F48.fromString('100');
export const MAX_I80F48 = new I80F48(I80F48.MAX_BN);
const _NEG_ONE_I80F48 = I80F48.fromNumber(-1);
export function ONE_I80F48(): I80F48 {
return I80F48.fromNumber(1);
}
export function ZERO_I80F48(): I80F48 {
return I80F48.fromNumber(0);
}
export function HUNDRED_I80F48(): I80F48 {
return I80F48.fromNumber(100);
}
export function MAX_I80F48(): I80F48 {
return new I80F48(I80F48.MAX_BN);
}

View File

@ -286,8 +286,8 @@ export class Bank {
const totalBorrows = this.nativeBorrows();
const totalDeposits = this.nativeDeposits();
if (totalDeposits.eq(ZERO_I80F48) && totalBorrows.eq(ZERO_I80F48)) {
return ZERO_I80F48;
if (totalDeposits.isZero() && totalBorrows.isZero()) {
return ZERO_I80F48();
}
if (totalDeposits.lte(totalBorrows)) {
return this.maxRate;
@ -329,9 +329,9 @@ export class Bank {
const totalBorrows = this.nativeBorrows();
const totalDeposits = this.nativeDeposits();
if (totalDeposits.eq(ZERO_I80F48) && totalBorrows.eq(ZERO_I80F48)) {
return ZERO_I80F48;
} else if (totalDeposits.eq(ZERO_I80F48)) {
if (totalDeposits.isZero() && totalBorrows.isZero()) {
return ZERO_I80F48();
} else if (totalDeposits.isZero()) {
return this.maxRate;
}

View File

@ -1,6 +1,11 @@
import { BorshAccountsCoder } from '@project-serum/anchor';
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
import { Market } from '@project-serum/serum';
import {
getFeeRates,
getFeeTier,
Market,
Orderbook,
} from '@project-serum/serum';
import { parsePriceData, PriceData } from '@pythnetwork/client';
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
@ -68,8 +73,9 @@ export class Group {
public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>,
public banksMapByTokenIndex: Map<number, Bank[]>,
public serum3MarketsMap: Map<string, Serum3Market>,
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
public serum3MarketExternalsMap: Map<string, Market>,
// TODO rethink key
public perpMarketsMap: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
public mintInfosMapByMint: Map<string, MintInfo>,
@ -77,12 +83,6 @@ export class Group {
public vaultAmountsMap: Map<string, number>,
) {}
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
return Array.from(this.serum3MarketsMap.values()).find(
(serum3Market) => serum3Market.marketIndex === marketIndex,
);
}
public async reloadAll(client: MangoClient) {
let ids: Id | undefined = undefined;
@ -180,14 +180,17 @@ export class Group {
serum3Markets = await client.serum3GetMarkets(this);
}
this.serum3MarketsMap = new Map(
serum3Markets.map((serum3Market) => [serum3Market.name, serum3Market]),
this.serum3MarketsMapByExternal = new Map(
serum3Markets.map((serum3Market) => [
serum3Market.serumMarketExternal.toBase58(),
serum3Market,
]),
);
}
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
const externalMarkets = await Promise.all(
Array.from(this.serum3MarketsMap.values()).map((serum3Market) =>
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
Market.load(
client.program.provider.connection,
serum3Market.serumMarketExternal,
@ -198,10 +201,12 @@ export class Group {
);
this.serum3MarketExternalsMap = new Map(
Array.from(this.serum3MarketsMap.values()).map((serum3Market, index) => [
serum3Market.name,
externalMarkets[index],
]),
Array.from(this.serum3MarketsMapByExternal.values()).map(
(serum3Market, index) => [
serum3Market.serumMarketExternal.toBase58(),
externalMarkets[index],
],
),
);
}
@ -237,7 +242,7 @@ export class Group {
for (const [index, price] of prices.entries()) {
for (const bank of banks[index]) {
if (bank.name === 'USDC') {
bank.price = ONE_I80F48;
bank.price = ONE_I80F48();
bank.uiPrice = 1;
} else {
// TODO: Implement switchboard oracle type
@ -299,14 +304,14 @@ export class Group {
);
}
public getMintDecimals(mintPk: PublicKey) {
public getMintDecimals(mintPk: PublicKey): number {
const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks)
throw new Error(`Unable to find mint decimals for ${mintPk.toString()}`);
return banks[0].mintDecimals;
}
public getFirstBankByMint(mintPk: PublicKey) {
public getFirstBankByMint(mintPk: PublicKey): Bank {
const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
return banks[0];
@ -340,6 +345,49 @@ export class Group {
return I80F48.fromNumber(totalAmount);
}
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.marketIndex === marketIndex,
);
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return await serum3Market.loadBids(client, this);
}
public async loadSerum3AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return await serum3Market.loadAsks(client, this);
}
public getFeeRate(maker = true) {
// TODO: fetch msrm/srm vault balance
const feeTier = getFeeTier(0, 0);
const rates = getFeeRates(feeTier);
return maker ? rates.maker : rates.taker;
}
/**
*
* @param mintPk

View File

@ -11,6 +11,7 @@ import {
ZERO_I80F48,
} from './I80F48';
import { HealthType } from './mangoAccount';
import { Serum3Market, Serum3Side } from './serum3';
// ░░░░
//
@ -37,42 +38,46 @@ 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(
public tokenInfos: TokenInfo[],
public serum3Infos: Serum3Info[],
public perpInfos: PerpInfo[],
) {}
constructor(dto: HealthCacheDto) {
this.tokenInfos = dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto));
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto));
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto));
static fromDto(dto) {
return new HealthCache(
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
dto.perpInfos.map((dto) => new PerpInfo(dto)),
);
}
public health(healthType: HealthType): I80F48 {
let health = ZERO_I80F48;
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
health = health.add(contrib);
health.iadd(contrib);
}
for (const serum3Info of this.serum3Infos) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
);
health = health.add(contrib);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
health = health.add(contrib);
health.iadd(contrib);
}
return health;
}
public assets(healthType: HealthType): I80F48 {
let assets = ZERO_I80F48;
const assets = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
assets.iadd(contrib);
}
}
for (const serum3Info of this.serum3Infos) {
@ -81,24 +86,24 @@ export class HealthCache {
this.tokenInfos,
);
if (contrib.isPos()) {
assets = assets.add(contrib);
assets.iadd(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
assets.iadd(contrib);
}
}
return assets;
}
public liabs(healthType: HealthType): I80F48 {
let liabs = ZERO_I80F48;
const liabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs = liabs.sub(contrib);
liabs.isub(contrib);
}
}
for (const serum3Info of this.serum3Infos) {
@ -107,28 +112,28 @@ export class HealthCache {
this.tokenInfos,
);
if (contrib.isNeg()) {
liabs = liabs.sub(contrib);
liabs.isub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs = liabs.sub(contrib);
liabs.isub(contrib);
}
}
return liabs;
}
public healthRatio(healthType: HealthType): I80F48 {
let assets = ZERO_I80F48;
let liabs = ZERO_I80F48;
const assets = ZERO_I80F48();
const liabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
assets.iadd(contrib);
} else {
liabs = liabs.sub(contrib);
liabs.isub(contrib);
}
}
for (const serum3Info of this.serum3Infos) {
@ -137,24 +142,24 @@ export class HealthCache {
this.tokenInfos,
);
if (contrib.isPos()) {
assets = assets.add(contrib);
assets.iadd(contrib);
} else {
liabs = liabs.sub(contrib);
liabs.isub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets = assets.add(contrib);
assets.iadd(contrib);
} else {
liabs = liabs.sub(contrib);
liabs.isub(contrib);
}
}
if (liabs.isPos()) {
return HUNDRED_I80F48.mul(assets.sub(liabs).div(liabs));
return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs));
} else {
return MAX_I80F48;
return MAX_I80F48();
}
}
@ -172,10 +177,50 @@ export class HealthCache {
return this.findTokenInfoIndex(bank.tokenIndex);
}
private static logHealthCache(debug: string, healthCache: HealthCache) {
console.log(debug);
adjustSerum3Reserved(
// todo change indices to types from numbers
marketIndex: number,
baseTokenIndex: number,
reservedBaseChange: I80F48,
freeBaseChange: I80F48,
quoteTokenIndex: number,
reservedQuoteChange: I80F48,
freeQuoteChange: I80F48,
) {
const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex);
const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex);
const baseEntry = this.tokenInfos[baseEntryIndex];
const reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice);
const quoteEntry = this.tokenInfos[quoteEntryIndex];
reservedAmount.iadd(reservedQuoteChange.mul(quoteEntry.oraclePrice));
// Apply it to the tokens
baseEntry.serum3MaxReserved.iadd(reservedAmount);
baseEntry.balance.iadd(freeBaseChange.mul(baseEntry.oraclePrice));
quoteEntry.serum3MaxReserved.iadd(reservedAmount);
quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice));
// Apply it to the serum3 info
const serum3Info = this.serum3Infos.find(
(serum3Info) => serum3Info.marketIndex === marketIndex,
);
if (!serum3Info) {
throw new Error(
`Serum3Info not found for market with index ${marketIndex}`,
);
}
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
}
public static logHealthCache(debug: string, healthCache: HealthCache) {
if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) {
console.log(`${token.toString()}`);
console.log(` ${token.toString()}`);
}
for (const serum3Info of healthCache.serum3Infos) {
console.log(` ${serum3Info.toString(healthCache.tokenInfos)}`);
}
console.log(
` assets ${healthCache.assets(
@ -209,14 +254,132 @@ export class HealthCache {
throw new Error(
`Oracle price not loaded for ${change.mintPk.toString()}`,
);
adjustedCache.tokenInfos[changeIndex].balance = adjustedCache.tokenInfos[
changeIndex
].balance.add(change.nativeTokenAmount.mul(bank.price));
adjustedCache.tokenInfos[changeIndex].balance.iadd(
change.nativeTokenAmount.mul(bank.price),
);
}
// HealthCache.logHealthCache('afterChange', adjustedCache);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3BidChanges(
group: Group,
bidNativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
if (!quoteBank) {
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
}
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
const quote = adjustedCache.tokenInfos[quoteIndex];
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for quote
adjustedCache.tokenInfos[quoteIndex].balance.isub(
bidNativeQuoteAmount.mul(quote.oraclePrice),
);
// Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
ZERO_I80F48(),
ZERO_I80F48(),
serum3Market.quoteTokenIndex,
bidNativeQuoteAmount,
ZERO_I80F48(),
);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3AskChanges(
group: Group,
askNativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!baseBank) {
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
}
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
const base = adjustedCache.tokenInfos[baseIndex];
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for base
adjustedCache.tokenInfos[baseIndex].balance.isub(
askNativeBaseAmount.mul(base.oraclePrice),
);
// Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
askNativeBaseAmount,
ZERO_I80F48(),
serum3Market.quoteTokenIndex,
ZERO_I80F48(),
ZERO_I80F48(),
);
return adjustedCache.healthRatio(healthType);
}
private static binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
right: I80F48,
rightRatio: I80F48,
targetRatio: I80F48,
healthRatioAfterActionFn: (I80F48) => I80F48,
) {
const maxIterations = 40;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromNumber(0.001);
if (
(leftRatio.sub(targetRatio).isPos() &&
rightRatio.sub(targetRatio).isPos()) ||
(leftRatio.sub(targetRatio).isNeg() &&
rightRatio.sub(targetRatio).isNeg())
) {
throw new Error(
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
);
}
let newAmount;
for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = healthRatioAfterActionFn(newAmount);
const error = newAmountRatio.sub(targetRatio);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
left = newAmount;
} else {
right = newAmount;
rightRatio = newAmountRatio;
}
}
console.error(
`Unable to get targetRatio within ${maxIterations} iterations`,
);
return newAmount;
}
getMaxSourceForTokenSwap(
group: Group,
sourceMintPk: PublicKey,
@ -227,20 +390,20 @@ export class HealthCache {
const targetBank: Bank = group.getFirstBankByMint(targetMintPk);
if (sourceMintPk.equals(targetMintPk)) {
return ZERO_I80F48;
return ZERO_I80F48();
}
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48)) {
return ZERO_I80F48;
if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
if (
sourceBank.initLiabWeight
.sub(targetBank.initAssetWeight)
.abs()
.lte(ZERO_I80F48)
.lte(ZERO_I80F48())
) {
return ZERO_I80F48;
return ZERO_I80F48();
}
// The health_ratio is a nonlinear based on swap amount.
@ -252,8 +415,8 @@ export class HealthCache {
// - be careful about finding the minRatio point: the function isn't convex
const initialRatio = this.healthRatio(HealthType.init);
if (initialRatio.lte(ZERO_I80F48)) {
return ZERO_I80F48;
if (initialRatio.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const healthCacheClone: HealthCache = _.cloneDeep(this);
@ -271,10 +434,8 @@ export class HealthCache {
function cacheAfterSwap(amount: I80F48) {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
// HealthCache.logHealthCache('beforeSwap', adjustedCache);
adjustedCache.tokenInfos[sourceIndex].balance =
adjustedCache.tokenInfos[sourceIndex].balance.sub(amount);
adjustedCache.tokenInfos[targetIndex].balance =
adjustedCache.tokenInfos[targetIndex].balance.add(amount);
adjustedCache.tokenInfos[sourceIndex].balance.isub(amount);
adjustedCache.tokenInfos[targetIndex].balance.iadd(amount);
// HealthCache.logHealthCache('afterSwap', adjustedCache);
return adjustedCache;
}
@ -285,60 +446,16 @@ export class HealthCache {
const point0Amount = source.balance
.min(target.balance.neg())
.max(ZERO_I80F48);
.max(ZERO_I80F48());
const point1Amount = source.balance
.max(target.balance.neg())
.max(ZERO_I80F48);
.max(ZERO_I80F48());
const cache0 = cacheAfterSwap(point0Amount);
const point0Ratio = cache0.healthRatio(HealthType.init);
const point0Health = cache0.health(HealthType.init);
const cache1 = cacheAfterSwap(point1Amount);
const point1Ratio = cache1.healthRatio(HealthType.init);
const point1Health = cache1.health(HealthType.init);
function binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
right: I80F48,
rightRatio: I80F48,
targetRatio: I80F48,
) {
const maxIterations = 20;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromString('0.001');
if (
(leftRatio.sub(targetRatio).isPos() &&
rightRatio.sub(targetRatio).isPos()) ||
(leftRatio.sub(targetRatio).isNeg() &&
rightRatio.sub(targetRatio).isNeg())
) {
throw new Error(
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
);
}
let newAmount;
for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromString('0.5'));
const newAmountRatio = healthRatioAfterSwap(newAmount);
const error = newAmountRatio.sub(targetRatio);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
left = newAmount;
} else {
right = newAmount;
rightRatio = newAmountRatio;
}
}
console.error(
`Unable to get targetRatio within ${maxIterations} iterations`,
);
return newAmount;
}
let amount: I80F48;
if (
@ -356,43 +473,37 @@ export class HealthCache {
} else if (point1Ratio.gt(initialRatio)) {
amount = point1Amount;
} else {
amount = ZERO_I80F48;
amount = ZERO_I80F48();
}
} else if (point1Ratio.gte(minRatio)) {
// If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount
// search to the right of point1Amount: but how far?
// At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for
// zero health: health - source_liab_weight * a + target_asset_weight * a = 0.
if (point1Health.lte(ZERO_I80F48)) {
return ZERO_I80F48;
if (point1Health.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const zeroHealthAmount = point1Amount.add(
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
);
// console.log(`point1Amount ${point1Amount}`);
// console.log(`point1Health ${point1Health}`);
// console.log(`point1Ratio ${point1Ratio}`);
// console.log(`point0Amount ${point0Amount}`);
// console.log(`point0Health ${point0Health}`);
// console.log(`point0Ratio ${point0Ratio}`);
// console.log(`zeroHealthAmount ${zeroHealthAmount}`);
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
// console.log(`zeroHealthRatio ${zeroHealthRatio}`);
amount = binaryApproximationSearch(
amount = HealthCache.binaryApproximationSearch(
point1Amount,
point1Ratio,
zeroHealthAmount,
zeroHealthRatio,
minRatio,
healthRatioAfterSwap,
);
} else if (point0Ratio.gte(minRatio)) {
// Must be between point0Amount and point1Amount.
amount = binaryApproximationSearch(
amount = HealthCache.binaryApproximationSearch(
point0Amount,
point0Ratio,
point1Amount,
point1Ratio,
minRatio,
healthRatioAfterSwap,
);
} else {
throw new Error(
@ -403,11 +514,131 @@ export class HealthCache {
return amount
.div(source.oraclePrice)
.div(
ONE_I80F48.add(
ONE_I80F48().add(
group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate,
),
);
}
getMaxForSerum3Order(
group: Group,
serum3Market: Serum3Market,
side: Serum3Side,
minRatio: I80F48,
) {
const baseBank = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
);
if (!baseBank) {
throw new Error(`No bank for index ${serum3Market.baseTokenIndex}`);
}
const quoteBank = group.getFirstBankByTokenIndex(
serum3Market.quoteTokenIndex,
);
if (!quoteBank) {
throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`);
}
const healthCacheClone: HealthCache = _.cloneDeep(this);
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank);
const base = healthCacheClone.tokenInfos[baseIndex];
const quote = healthCacheClone.tokenInfos[quoteIndex];
// Binary search between current health (0 sized new order) and
// an amount to trade which will bring health to 0.
// Current health and amount i.e. 0
const initialAmount = ZERO_I80F48();
const initialHealth = this.health(HealthType.init);
const initialRatio = this.healthRatio(HealthType.init);
if (initialRatio.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
// Amount which would bring health to 0
// amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight)
// A is what we would be essentially swapping for B
// So when its an ask, then base->quote,
// and when its a bid, then quote->bid
let zeroAmount;
if (side == Serum3Side.ask) {
const quoteBorrows = quote.balance.lt(ZERO_I80F48())
? quote.balance.abs()
: ZERO_I80F48();
zeroAmount = base.balance
.max(quoteBorrows)
.add(
initialHealth.div(
base
.liabWeight(HealthType.init)
.sub(quote.assetWeight(HealthType.init)),
),
);
} else {
const baseBorrows = base.balance.lt(ZERO_I80F48())
? base.balance.abs()
: ZERO_I80F48();
zeroAmount = quote.balance
.max(baseBorrows)
.add(
initialHealth.div(
quote
.liabWeight(HealthType.init)
.sub(base.assetWeight(HealthType.init)),
),
);
}
const cache = cacheAfterPlacingOrder(zeroAmount);
const zeroAmountRatio = cache.healthRatio(HealthType.init);
function cacheAfterPlacingOrder(amount: I80F48) {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
side === Serum3Side.ask
? adjustedCache.tokenInfos[baseIndex].balance.isub(amount)
: adjustedCache.tokenInfos[quoteIndex].balance.isub(amount);
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(),
ZERO_I80F48(),
serum3Market.quoteTokenIndex,
side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48(),
ZERO_I80F48(),
);
return adjustedCache;
}
function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 {
return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init);
}
const amount = HealthCache.binaryApproximationSearch(
initialAmount,
initialRatio,
zeroAmount,
zeroAmountRatio,
minRatio,
healthRatioAfterPlacingOrder,
);
// If its a bid then the reserved fund and potential loan is in quote,
// If its a ask then the reserved fund and potential loan is in base,
// also keep some buffer for fees, use taker fees for worst case simulation.
return side === Serum3Side.bid
? amount
.div(quote.oraclePrice)
.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))))
: amount
.div(base.oraclePrice)
.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate))
.div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false))));
}
}
export class TokenInfo {
@ -450,8 +681,8 @@ export class TokenInfo {
bank.maintLiabWeight,
bank.initLiabWeight,
bank.price,
ZERO_I80F48,
ZERO_I80F48,
ZERO_I80F48(),
ZERO_I80F48(),
);
}
@ -476,20 +707,30 @@ export class TokenInfo {
}
toString() {
return ` tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`;
return ` tokenIndex: ${this.tokenIndex}, balance: ${
this.balance
}, serum3MaxReserved: ${
this.serum3MaxReserved
}, initHealth ${this.healthContribution(HealthType.init)}`;
}
}
export class Serum3Info {
constructor(dto: Serum3InfoDto) {
this.reserved = I80F48.from(dto.reserved);
this.baseIndex = dto.baseIndex;
this.quoteIndex = dto.quoteIndex;
}
constructor(
public reserved: I80F48,
public baseIndex: number,
public quoteIndex: number,
public marketIndex: number,
) {}
reserved: I80F48;
baseIndex: number;
quoteIndex: number;
static fromDto(dto: Serum3InfoDto) {
return new Serum3Info(
I80F48.from(dto.reserved),
dto.baseIndex,
dto.quoteIndex,
dto.marketIndex,
);
}
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
const baseInfo = tokenInfos[this.baseIndex];
@ -497,7 +738,7 @@ export class Serum3Info {
const reserved = this.reserved;
if (reserved.isZero()) {
return ZERO_I80F48;
return ZERO_I80F48();
}
// How much the health would increase if the reserved balance were applied to the passed
@ -512,15 +753,14 @@ export class Serum3Info {
let assetPart, liabPart;
if (maxBalance.gte(reserved)) {
assetPart = reserved;
liabPart = ZERO_I80F48;
liabPart = ZERO_I80F48();
} else if (maxBalance.isNeg()) {
assetPart = ZERO_I80F48;
assetPart = ZERO_I80F48();
liabPart = reserved;
} else {
assetPart = maxBalance;
liabPart = reserved.sub(maxBalance);
}
const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(healthType);
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
@ -530,6 +770,14 @@ export class Serum3Info {
const reservedAsQuote = computeHealthEffect(quoteInfo);
return reservedAsBase.min(reservedAsQuote);
}
toString(tokenInfos: TokenInfo[]) {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
this.baseIndex
}, quoteIndex: ${this.quoteIndex}, reserved: ${
this.reserved
}, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`;
}
}
export class PerpInfo {
@ -566,7 +814,7 @@ export class PerpInfo {
// 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);
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48());
}
}
@ -586,12 +834,39 @@ export class TokenInfoDto {
balance: I80F48Dto;
// in health-reference-token native units
serum3MaxReserved: I80F48Dto;
constructor(
tokenIndex: number,
maintAssetWeight: I80F48Dto,
initAssetWeight: I80F48Dto,
maintLiabWeight: I80F48Dto,
initLiabWeight: I80F48Dto,
oraclePrice: I80F48Dto,
balance: I80F48Dto,
serum3MaxReserved: I80F48Dto,
) {
this.tokenIndex = tokenIndex;
this.maintAssetWeight = maintAssetWeight;
this.initAssetWeight = initAssetWeight;
this.maintLiabWeight = maintLiabWeight;
this.initLiabWeight = initLiabWeight;
this.oraclePrice = oraclePrice;
this.balance = balance;
this.serum3MaxReserved = serum3MaxReserved;
}
}
export class Serum3InfoDto {
reserved: I80F48Dto;
baseIndex: number;
quoteIndex: number;
marketIndex: number;
constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) {
this.reserved = reserved;
this.baseIndex = baseIndex;
this.quoteIndex = quoteIndex;
}
}
export class PerpInfoDto {

View File

@ -1,5 +1,6 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { Order, Orderbook } from '@project-serum/serum/lib/market';
import { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
@ -7,6 +8,7 @@ import { Bank } from './bank';
import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { Serum3Market, Serum3Side } from './serum3';
export class MangoAccount {
public tokens: TokenPosition[];
public serum3: Serum3Orders[];
@ -91,6 +93,18 @@ export class MangoAccount {
return this;
}
tokensActive(): TokenPosition[] {
return this.tokens.filter((token) => token.isActive());
}
serum3Active(): Serum3Orders[] {
return this.serum3.filter((serum3) => serum3.isActive());
}
perpActive(): PerpPosition[] {
return this.perps.filter((perp) => perp.isActive());
}
findToken(tokenIndex: number): TokenPosition | undefined {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
}
@ -112,7 +126,7 @@ export class MangoAccount {
*/
getTokenBalance(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex);
return tp ? tp.balance(bank) : ZERO_I80F48;
return tp ? tp.balance(bank) : ZERO_I80F48();
}
/**
@ -122,7 +136,7 @@ export class MangoAccount {
*/
getTokenDeposits(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex);
return tp ? tp.deposits(bank) : ZERO_I80F48;
return tp ? tp.deposits(bank) : ZERO_I80F48();
}
/**
@ -132,7 +146,7 @@ export class MangoAccount {
*/
getTokenBorrows(bank: Bank): I80F48 {
const tp = this.findToken(bank.tokenIndex);
return tp ? tp.borrows(bank) : ZERO_I80F48;
return tp ? tp.borrows(bank) : ZERO_I80F48();
}
/**
@ -209,7 +223,7 @@ export class MangoAccount {
const equity = this.accountData.equity;
const total_equity = equity.tokens.reduce(
(a, b) => a.add(b.value),
ZERO_I80F48,
ZERO_I80F48(),
);
return total_equity;
}
@ -255,8 +269,8 @@ export class MangoAccount {
// Case 1:
// Cannot withdraw if init health is below 0
if (initHealth.lte(ZERO_I80F48)) {
return ZERO_I80F48;
if (initHealth.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
// Deposits need special treatment since they would neither count towards liabilities
@ -264,12 +278,12 @@ export class MangoAccount {
const tp = this.findToken(tokenBank.tokenIndex);
if (!tokenBank.price) return undefined;
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48;
let existingPositionHealthContrib = ZERO_I80F48;
if (existingTokenDeposits.gt(ZERO_I80F48)) {
existingPositionHealthContrib = existingTokenDeposits
.mul(tokenBank.price)
.mul(tokenBank.initAssetWeight);
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48();
const existingPositionHealthContrib = ZERO_I80F48();
if (existingTokenDeposits.gt(ZERO_I80F48())) {
existingTokenDeposits
.imul(tokenBank.price)
.imul(tokenBank.initAssetWeight);
}
// Case 2: token deposits have higher contribution than initHealth,
@ -296,7 +310,7 @@ export class MangoAccount {
.div(tokenBank.initLiabWeight)
.div(tokenBank.price);
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
ONE_I80F48.add(tokenBank.loanOriginationFeeRate),
ONE_I80F48().add(tokenBank.loanOriginationFeeRate),
);
// console.log(`initHealth ${initHealth}`);
// console.log(
@ -344,7 +358,7 @@ export class MangoAccount {
group,
sourceMintPk,
targetMintPk,
ONE_I80F48, // target 1% health
ONE_I80F48(), // target 1% health
)
.mul(I80F48.fromNumber(slippageAndFeesFactor));
}
@ -431,25 +445,234 @@ export class MangoAccount {
.toNumber();
}
/**
* The remaining native quote margin available for given market.
*
* TODO: this is a very bad estimation atm.
* It assumes quote asset is always quote,
* it assumes that there are no interaction effects,
* it assumes that there are no existing borrows for either of the tokens in the market.
*/
getSerum3MarketMarginAvailable(
public async loadSerum3OpenOrdersForMarket(
client: MangoClient,
group: Group,
marketName: string,
): I80F48 | undefined {
if (!this.accountData) return undefined;
const initHealth = this.accountData.initHealth;
const serum3Market = group.serum3MarketsMap.get(marketName)!;
const marketAssetWeight = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
).initAssetWeight;
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
externalMarketPk: PublicKey,
): Promise<Order[]> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
const serum3OO = this.serum3Active().find(
(s) => s.marketIndex === serum3Market.marketIndex,
);
if (!serum3OO) {
throw new Error(`No open orders account found for ${externalMarketPk}`);
}
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
const [bidsInfo, asksInfo] =
await client.program.provider.connection.getMultipleAccountsInfo([
serum3MarketExternal.bidsAddress,
serum3MarketExternal.asksAddress,
]);
if (!bidsInfo || !asksInfo) {
throw new Error(
`bids and asks ai were not fetched for ${externalMarketPk.toString()}`,
);
}
const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
const asks = Orderbook.decode(serum3MarketExternal, asksInfo.data);
return [...bids, ...asks].filter((o) =>
o.openOrdersAddress.equals(serum3OO.openOrders),
);
}
/**
*
* @param group
* @param serum3Market
* @returns maximum native quote which can be traded for base token given current health
*/
public getMaxQuoteForSerum3Bid(
group: Group,
serum3Market: Serum3Market,
): I80F48 {
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache.getMaxForSerum3Order(
group,
serum3Market,
Serum3Side.bid,
I80F48.fromNumber(3),
);
}
public getMaxQuoteForSerum3BidUi(
group: Group,
externalMarketPk: PublicKey,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market);
return toUiDecimals(
nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
);
}
/**
*
* @param group
* @param serum3Market
* @returns maximum native base which can be traded for quote token given current health
*/
public getMaxBaseForSerum3Ask(
group: Group,
serum3Market: Serum3Market,
): I80F48 {
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache.getMaxForSerum3Order(
group,
serum3Market,
Serum3Side.ask,
I80F48.fromNumber(3),
);
}
public getMaxBaseForSerum3AskUi(
group: Group,
externalMarketPk: PublicKey,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market);
return toUiDecimals(
nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
);
}
/**
*
* @param group
* @param nativeQuoteAmount
* @param serum3Market
* @param healthType
* @returns health ratio after a bid with nativeQuoteAmount is placed
*/
simHealthRatioWithSerum3BidChanges(
group: Group,
nativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache.simHealthRatioWithSerum3BidChanges(
group,
nativeQuoteAmount,
serum3Market,
healthType,
);
}
simHealthRatioWithSerum3BidUiChanges(
group: Group,
uiQuoteAmount: number,
externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return this.simHealthRatioWithSerum3BidChanges(
group,
toNative(
uiQuoteAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.mintDecimals,
),
serum3Market,
healthType,
).toNumber();
}
/**
*
* @param group
* @param nativeBaseAmount
* @param serum3Market
* @param healthType
* @returns health ratio after an ask with nativeBaseAmount is placed
*/
simHealthRatioWithSerum3AskChanges(
group: Group,
nativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
if (!this.accountData) {
throw new Error(
`accountData not loaded on MangoAccount, try reloading MangoAccount`,
);
}
return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges(
group,
nativeBaseAmount,
serum3Market,
healthType,
);
}
simHealthRatioWithSerum3AskUiChanges(
group: Group,
uiBaseAmount: number,
externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return this.simHealthRatioWithSerum3AskChanges(
group,
toNative(
uiBaseAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.mintDecimals,
),
serum3Market,
healthType,
).toNumber();
}
/**
@ -468,19 +691,7 @@ export class MangoAccount {
const initHealth = this.accountData.initHealth;
const perpMarket = group.perpMarketsMap.get(marketName)!;
const marketAssetWeight = perpMarket.initAssetWeight;
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
}
tokensActive(): TokenPosition[] {
return this.tokens.filter((token) => token.isActive());
}
serum3Active(): Serum3Orders[] {
return this.serum3.filter((serum3) => serum3.isActive());
}
perpActive(): PerpPosition[] {
return this.perps.filter((perp) => perp.isActive());
return initHealth.div(ONE_I80F48().sub(marketAssetWeight));
}
toString(group?: Group): string {
@ -558,8 +769,8 @@ export class TokenPosition {
* @returns native deposits, 0 if position has borrows
*/
public deposits(bank: Bank): I80F48 {
if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48)) {
return ZERO_I80F48;
if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48())) {
return ZERO_I80F48();
}
return this.balance(bank);
}
@ -570,8 +781,8 @@ export class TokenPosition {
* @returns native borrows, 0 if position has deposits
*/
public borrows(bank: Bank): I80F48 {
if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48)) {
return ZERO_I80F48;
if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48())) {
return ZERO_I80F48();
}
return this.balance(bank).abs();
}
@ -734,7 +945,7 @@ export class MangoAccountData {
tokenAssets: any;
}) {
return new MangoAccountData(
new HealthCache(event.healthCache),
HealthCache.fromDto(event.healthCache),
I80F48.from(event.initHealth),
I80F48.from(event.maintHealth),
Equity.from(event.equity),

View File

@ -1,6 +1,11 @@
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { Market, Orderbook } from '@project-serum/serum/lib/market';
import { Cluster, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { Group } from './group';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
export class Serum3Market {
public name: string;
@ -44,6 +49,117 @@ export class Serum3Market {
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
}
/**
*
* @param group
* @returns maximum leverage one can bid on this market, this is only for display purposes,
* also see getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi
*/
maxBidLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
if (!baseBank) {
throw new Error(
`bank for base token with index ${this.baseTokenIndex} not found`,
);
}
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if (
quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48())
) {
return MAX_I80F48().toNumber();
}
return ONE_I80F48()
.div(quoteBank.initLiabWeight.sub(baseBank.initAssetWeight))
.toNumber();
}
/**
*
* @param group
* @returns maximum leverage one can ask on this market, this is only for display purposes,
* also see getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi
*/
maxAskLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
if (!baseBank) {
throw new Error(
`bank for base token with index ${this.baseTokenIndex} not found`,
);
}
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if (
baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48())
) {
return MAX_I80F48().toNumber();
}
return ONE_I80F48()
.div(baseBank.initLiabWeight.sub(quoteBank.initAssetWeight))
.toNumber();
}
public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
);
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadBids(
client.program.provider.connection,
);
}
public async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
);
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadAsks(
client.program.provider.connection,
);
}
public async logOb(client: MangoClient, group: Group): Promise<string> {
let res = ``;
res += ` ${this.name} OrderBook`;
let orders = await this?.loadAsks(client, group);
for (const order of orders!.items(true)) {
res += `\n ${order.price.toString().padStart(10)}, ${order.size
.toString()
.padStart(10)}`;
}
res += `\n --------------------------`;
orders = await this?.loadBids(client, group);
for (const order of orders!.items(true)) {
res += `\n ${order.price.toString().padStart(10)}, ${order.size
.toString()
.padStart(10)}`;
}
return res;
}
}
export class Serum3SelfTradeBehavior {
@ -62,3 +178,21 @@ export class Serum3Side {
static bid = { bid: {} };
static ask = { ask: {} };
}
export async function generateSerum3MarketExternalVaultSignerAddress(
cluster: Cluster,
serum3Market: Serum3Market,
serum3MarketExternal: Market,
): Promise<PublicKey> {
return await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
SERUM3_PROGRAM_ID[cluster],
);
}

View File

@ -1,6 +1,4 @@
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
import { getFeeRates, getFeeTier } from '@project-serum/serum';
import { Order } from '@project-serum/serum/lib/market';
import {
closeAccount,
initializeAccount,
@ -37,6 +35,7 @@ import {
import { StubOracle } from './accounts/oracle';
import { OrderType, PerpMarket, Side } from './accounts/perp';
import {
generateSerum3MarketExternalVaultSignerAddress,
Serum3Market,
Serum3OrderType,
Serum3SelfTradeBehavior,
@ -348,7 +347,7 @@ export class MangoClient {
}
return await this.program.methods
.tokenDeregister(bank.tokenIndex)
.tokenDeregister()
.accounts({
group: group.publicKey,
admin: adminPk,
@ -681,9 +680,13 @@ export class MangoClient {
mangoAccount: MangoAccount,
): Promise<MangoAccountData | undefined> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
mangoAccount,
]);
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
// Use our custom simulate fn in utils/anchor.ts so signing the tx is not required
this.program.provider.simulate = simulate;
@ -785,6 +788,7 @@ export class MangoClient {
group,
[mangoAccount],
[bank],
[],
);
const transaction = await this.program.methods
@ -794,6 +798,7 @@ export class MangoClient {
account: mangoAccount.publicKey,
bank: bank.publicKey,
vault: bank.vault,
oracle: bank.oracle,
tokenAccount: wrappedSolAccount?.publicKey ?? tokenAccountPk,
tokenAuthority: mangoAccount.owner,
})
@ -858,6 +863,7 @@ export class MangoClient {
group,
[mangoAccount],
[bank],
[],
);
const transaction = await this.program.methods
@ -867,6 +873,7 @@ export class MangoClient {
account: mangoAccount.publicKey,
bank: bank.publicKey,
vault: bank.vault,
oracle: bank.oracle,
tokenAccount: tokenAccountPk,
owner: mangoAccount.owner,
})
@ -922,9 +929,11 @@ export class MangoClient {
public async serum3deregisterMarket(
group: Group,
serum3MarketName: string,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
return await this.program.methods
.serum3DeregisterMarket()
@ -986,7 +995,8 @@ export class MangoClient {
mangoAccount: MangoAccount,
marketName: string,
): Promise<TransactionSignature> {
const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!;
const serum3Market: Serum3Market =
group.serum3MarketsMapByExternal.get(marketName)!;
return await this.program.methods
.serum3CreateOpenOrders()
@ -1005,9 +1015,11 @@ export class MangoClient {
public async serum3CloseOpenOrders(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const openOrders = mangoAccount.serum3.find(
(account) => account.marketIndex === serum3Market.marketIndex,
@ -1031,7 +1043,7 @@ export class MangoClient {
public async serum3PlaceOrder(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
side: Serum3Side,
price: number,
size: number,
@ -1040,46 +1052,48 @@ export class MangoClient {
clientOrderId: number,
limit: number,
) {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC');
mangoAccount = await this.getMangoAccount(mangoAccount);
}
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
SERUM3_PROGRAM_ID[this.cluster],
await generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
mangoAccount,
]);
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
const feeTier = getFeeTier(0, 0 /** TODO: fix msrm/srm balance */);
const rates = getFeeRates(feeTier);
const maxQuoteQuantity = new BN(
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
(1 + rates.taker) /** TODO: fix taker/maker */,
).mul(
serum3MarketExternal
.baseSizeNumberToLots(size)
.mul(serum3MarketExternal.priceNumberToLots(price)),
);
const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize
.mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly)))
.mul(
serum3MarketExternal
.baseSizeNumberToLots(size)
.mul(serum3MarketExternal.priceNumberToLots(price)),
);
const payerTokenIndex = (() => {
if (side == Serum3Side.bid) {
return serum3Market.quoteTokenIndex;
} else {
return serum3Market.baseTokenIndex;
}
})();
return await this.program.methods
.serum3PlaceOrder(
@ -1108,14 +1122,8 @@ export class MangoClient {
marketBaseVault: serum3MarketExternal.decoded.baseVault,
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
marketVaultSigner: serum3MarketExternalVaultSigner,
quoteBank: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.publicKey,
quoteVault: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.vault,
baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.publicKey,
baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.vault,
payerBank: group.getFirstBankByTokenIndex(payerTokenIndex).publicKey,
payerVault: group.getFirstBankByTokenIndex(payerTokenIndex).vault,
})
.remainingAccounts(
healthRemainingAccounts.map(
@ -1129,13 +1137,16 @@ export class MangoClient {
async serum3CancelAllorders(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
limit: number,
) {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
return await this.program.methods
.serum3CancelAllOrders(limit)
@ -1158,25 +1169,19 @@ export class MangoClient {
async serum3SettleFunds(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
// TODO: put into a helper method, and remove copy pasta
await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
SERUM3_PROGRAM_ID[this.cluster],
await generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
);
return await this.program.methods
@ -1208,14 +1213,17 @@ export class MangoClient {
async serum3CancelOrder(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
side: Serum3Side,
orderId: BN,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
return await this.program.methods
.serum3CancelOrder(side, orderId)
@ -1234,20 +1242,6 @@ export class MangoClient {
.rpc();
}
async getSerum3Orders(
group: Group,
serum3MarketName: string,
): Promise<Order[]> {
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
// TODO: filter for mango account
return await serum3MarketExternal.loadOrdersForOwner(
this.program.provider.connection,
group.publicKey,
);
}
/// perps
async perpCreateMarket(
@ -1470,9 +1464,13 @@ export class MangoClient {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
mangoAccount,
]);
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[perpMarket],
);
const [nativePrice, nativeQuantity] = perpMarket.uiToNativePriceQuantity(
price,
@ -1543,6 +1541,7 @@ export class MangoClient {
group,
[mangoAccount],
[inputBank, outputBank],
[],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
@ -1727,6 +1726,7 @@ export class MangoClient {
group,
[liqor, liqee],
[assetBank, liabBank],
[],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
@ -1802,31 +1802,39 @@ export class MangoClient {
/// private
// todo make private
public buildHealthRemainingAccounts(
retriever: AccountRetriever,
group: Group,
mangoAccounts: MangoAccount[],
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
if (retriever === AccountRetriever.Fixed) {
return this.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccounts[0],
banks,
perpMarkets,
);
} else {
return this.buildScanningAccountRetrieverHealthAccounts(
group,
mangoAccounts,
banks,
perpMarkets,
);
}
}
// todo make private
public buildFixedAccountRetrieverHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
// Banks and perpMarkets for whom positions don't exist on mango account,
// but user would potentially open new positions.
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = [];
@ -1857,11 +1865,7 @@ export class MangoClient {
healthRemainingAccounts.push(
...mintInfos.map((mintInfo) => mintInfo.oracle),
);
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
healthRemainingAccounts.push(
...mangoAccount.perps
.filter((perp) => perp.marketIndex !== 65535)
@ -1872,14 +1876,36 @@ export class MangoClient {
)[0].publicKey,
),
);
for (const perpMarket of perpMarkets) {
const alreadyAdded = mangoAccount.perps.find(
(p) => p.marketIndex === perpMarket.perpMarketIndex,
);
if (!alreadyAdded) {
healthRemainingAccounts.push(
Array.from(group.perpMarketsMap.values()).filter(
(p) => p.perpMarketIndex === perpMarket.perpMarketIndex,
)[0].publicKey,
);
}
}
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
// debugHealthAccounts(group, mangoAccount, healthRemainingAccounts);
return healthRemainingAccounts;
}
// todo make private
public buildScanningAccountRetrieverHealthAccounts(
group: Group,
mangoAccounts: MangoAccount[],
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = [];
@ -1907,6 +1933,7 @@ export class MangoClient {
healthRemainingAccounts.push(
...mintInfos.map((mintInfo) => mintInfo.oracle),
);
for (const mangoAccount of mangoAccounts) {
healthRemainingAccounts.push(
...mangoAccount.serum3
@ -1914,6 +1941,7 @@ export class MangoClient {
.map((serum3Account) => serum3Account.openOrders),
);
}
for (const mangoAccount of mangoAccounts) {
healthRemainingAccounts.push(
...mangoAccount.perps
@ -1926,6 +1954,20 @@ export class MangoClient {
),
);
}
for (const mangoAccount of mangoAccounts) {
for (const perpMarket of perpMarkets) {
const alreadyAdded = mangoAccount.perps.find(
(p) => p.marketIndex === perpMarket.perpMarketIndex,
);
if (!alreadyAdded) {
healthRemainingAccounts.push(
Array.from(group.perpMarketsMap.values()).filter(
(p) => p.perpMarketIndex === perpMarket.perpMarketIndex,
)[0].publicKey,
);
}
}
}
return healthRemainingAccounts;
}

View File

@ -1,38 +1,47 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
import { Connection, Keypair } from '@solana/web3.js';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { I80F48, ZERO_I80F48 } from '../accounts/I80F48';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { toUiDecimals } from '../utils';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const PAYER_KEYPAIR =
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const GROUP_NUM = Number(process.env.GROUP_NUM || 2);
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
const connection = new Connection(CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
);
const adminWallet = new Wallet(admin);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
const group = await client.getGroupForCreator(admin.publicKey, 2);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`Group ${group.publicKey.toBase58()}`);
console.log(`${group.toString()}`);
const banks = Array.from(group.banksMapByMint.values()).flat();
const banksMapUsingTokenIndex = new Map(
banks.map((bank) => {
(bank as any).indexedDepositsByMangoAccounts = ZERO_I80F48;
(bank as any).indexedBorrowsByMangoAccounts = ZERO_I80F48;
(bank as any).indexedDepositsByMangoAccounts = ZERO_I80F48();
(bank as any).indexedBorrowsByMangoAccounts = ZERO_I80F48();
return [bank.tokenIndex, bank];
}),
);
@ -51,7 +60,7 @@ async function main() {
bank as any
).indexedDepositsByMangoAccounts.add(
token.indexedPosition.mul(
banksMapUsingTokenIndex.get(token.tokenIndex).depositIndex,
banksMapUsingTokenIndex.get(token.tokenIndex)!.depositIndex,
),
);
}
@ -61,7 +70,7 @@ async function main() {
).indexedBorrowsByMangoAccounts.add(
token.indexedPosition
.abs()
.mul(banksMapUsingTokenIndex.get(token.tokenIndex).borrowIndex),
.mul(banksMapUsingTokenIndex.get(token.tokenIndex)!.borrowIndex),
);
}
}),
@ -74,7 +83,7 @@ async function main() {
coder()
.accounts.decode(
'token',
(await client.program.provider.connection.getAccountInfo(bank.vault))
(await client.program.provider.connection.getAccountInfo(bank.vault))!
.data,
)
.amount.toNumber(),
@ -94,7 +103,7 @@ async function main() {
`\n ${'bank'.padEnd(40)} ${bank.publicKey}` +
`\n ${'vault'.padEnd(40)} ${bank.vault}` +
`\n ${'mint'.padEnd(40)} ${bank.mint}` +
`\n ${'price'.padEnd(40)} ${bank.price.toNumber()}` +
`\n ${'price'.padEnd(40)} ${bank.price?.toNumber()}` +
`\n ${'uiPrice'.padEnd(40)} ${bank.uiPrice}` +
`\n ${'error'.padEnd(40)} ${error}` +
`\n ${'collectedFeesNative'.padEnd(40)} ${bank.collectedFeesNative}` +

View File

@ -20,24 +20,29 @@ async function debugUser(
console.log(
'buildFixedAccountRetrieverHealthAccounts ' +
client
.buildFixedAccountRetrieverHealthAccounts(group, mangoAccount, [
group.banksMapByName.get('BTC')[0],
group.banksMapByName.get('USDC')[0],
])
.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccount,
[
group.banksMapByName.get('BTC')![0],
group.banksMapByName.get('USDC')![0],
],
[],
)
.map((pk) => pk.toBase58())
.join(', '),
);
console.log(
'mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity().toNumber()),
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
);
console.log(
'mangoAccount.getHealth(HealthType.init) ' +
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init).toNumber()),
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.init) ' +
mangoAccount.getHealthRatio(HealthType.init).toNumber(),
mangoAccount.getHealthRatio(HealthType.init)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.init) ' +
@ -45,7 +50,7 @@ async function debugUser(
);
console.log(
'mangoAccount.getHealthRatio(HealthType.maint) ' +
mangoAccount.getHealthRatio(HealthType.maint).toNumber(),
mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
@ -53,18 +58,18 @@ async function debugUser(
);
console.log(
'mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue().toNumber()),
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
);
console.log(
'mangoAccount.getAssetsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
),
);
console.log(
'mangoAccount.getLiabsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
),
);
@ -73,7 +78,7 @@ async function debugUser(
`mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` +
mangoAccount.getMaxWithdrawWithBorrowForTokenUi(
group,
group.banksMapByName.get(token)[0].mint,
group.banksMapByName.get(token)![0].mint,
),
);
}
@ -89,29 +94,28 @@ async function debugUser(
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} 1 `, {
mintPk: group.banksMapByName.get(srcToken)[0].mint,
mintPk: group.banksMapByName.get(srcToken)![0].mint,
uiTokenAmount: 1,
});
simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} -1 `, {
mintPk: group.banksMapByName.get(srcToken)[0].mint,
mintPk: group.banksMapByName.get(srcToken)![0].mint,
uiTokenAmount: -1,
});
}
function getMaxSourceForTokenSwapWrapper(src, tgt) {
// console.log();
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount
.getMaxSourceForTokenSwap(
group,
group.banksMapByName.get(src)[0].mint,
group.banksMapByName.get(tgt)[0].mint,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)
)!
.div(
I80F48.fromNumber(
Math.pow(10, group.banksMapByName.get(src)[0].mintDecimals),
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
),
)
.toNumber(),
@ -148,8 +152,8 @@ async function main() {
const group = await client.getGroupForCreator(admin.publicKey, 2);
for (const keypair of [
process.env.MB_PAYER_KEYPAIR,
process.env.MB_USER2_KEYPAIR,
process.env.MB_PAYER_KEYPAIR!,
process.env.MB_USER2_KEYPAIR!,
]) {
console.log();
const user = Keypair.fromSecretKey(

View File

@ -0,0 +1,290 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
// Reference
// https://explorer.solana.com/
// https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['USDT', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], // Wrapped Bitcoin (Sollet)
['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'], // Ether (Portal), will be treat as ETH due to higher liquidity
['soETH', '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk'], // Wrapped Ethereum (Sollet), will be treated as soETH
['SOL', 'So11111111111111111111111111111111111111112'],
['mSOL', 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
]);
// Reference
// https://pyth.network/price-feeds/
// https://switchboard.xyz/explorer
const MAINNET_ORACLES = new Map([
['USDT', '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL'],
['BTC', 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'],
['ETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'],
['soETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'],
['SOL', 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'],
['mSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'],
['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'],
]);
async function createGroup() {
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
);
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
);
console.log(`Creating Group...`);
const insuranceMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
await client.groupCreate(
GROUP_NUM,
true /* with intention */,
0 /* since spot and perp features are not finished */,
insuranceMint,
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`...registered group ${group.publicKey}`);
}
async function registerTokens() {
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
);
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts' /* idsjson service doesn't know about this group yet */,
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`Creating USDC stub oracle...`);
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
await client.stubOracleCreate(group, usdcMainnetMint, 1.0);
const usdcMainnetOracle = (
await client.getStubOracle(group, usdcMainnetMint)
)[0];
console.log(`...created stub oracle ${usdcMainnetOracle.publicKey}`);
console.log(`Registering USDC...`);
await client.tokenRegister(
group,
usdcMainnetMint,
usdcMainnetOracle.publicKey,
0.1,
0, // insurance vault token should be the first to be registered
'USDC',
0.004, // rate parameters are chosen to be the same for all high asset weight tokens,
// hoping that dynamic rate parameter adjustment would be enough to tune their rates to the markets needs
0.7,
0.1,
0.85,
0.2,
2.0,
0.005, // 50 bps
0.0005, // 5 bps
1,
1,
1,
1,
0,
);
console.log(`Registering USDT...`);
const usdtMainnetMint = new PublicKey(MAINNET_MINTS.get('USDT')!);
const usdtMainnetOracle = new PublicKey(MAINNET_ORACLES.get('USDT')!);
await client.tokenRegister(
group,
usdtMainnetMint,
usdtMainnetOracle,
0.1,
1,
'USDT',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.95,
0.9,
1.05,
1.1,
0.025, // rule of thumb used - half of maintLiabWeight
);
console.log(`Registering BTC...`);
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
const btcMainnetOracle = new PublicKey(MAINNET_ORACLES.get('BTC')!);
await client.tokenRegister(
group,
btcMainnetMint,
btcMainnetOracle,
0.1,
2,
'BTC',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
console.log(`Registering ETH...`);
const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
const ethMainnetOracle = new PublicKey(MAINNET_ORACLES.get('ETH')!);
await client.tokenRegister(
group,
ethMainnetMint,
ethMainnetOracle,
0.1,
3,
'ETH',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
console.log(`Registering soETH...`);
const soEthMainnetMint = new PublicKey(MAINNET_MINTS.get('soETH')!);
const soEthMainnetOracle = new PublicKey(MAINNET_ORACLES.get('soETH')!);
await client.tokenRegister(
group,
soEthMainnetMint,
soEthMainnetOracle,
0.1,
4,
'soETH',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
console.log(`Registering SOL...`);
const solMainnetMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const solMainnetOracle = new PublicKey(MAINNET_ORACLES.get('SOL')!);
await client.tokenRegister(
group,
solMainnetMint,
solMainnetOracle,
0.1,
5,
'SOL',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
console.log(`Registering mSOL...`);
const msolMainnetMint = new PublicKey(MAINNET_MINTS.get('mSOL')!);
const msolMainnetOracle = new PublicKey(MAINNET_ORACLES.get('mSOL')!);
await client.tokenRegister(
group,
msolMainnetMint,
msolMainnetOracle,
0.1,
6,
'mSOL',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
// log tokens/banks
await group.reloadAll(client);
for (const [bank] of await group.banksMapByMint.values()) {
console.log(`${bank.toString()}`);
}
}
async function main() {
createGroup();
registerTokens();
}
try {
main();
} catch (error) {
console.log(error);
}

View File

@ -730,12 +730,7 @@ export type MangoV4 = {
"isSigner": false
}
],
"args": [
{
"name": "tokenIndex",
"type": "u16"
}
]
"args": []
},
{
"name": "tokenUpdateIndexAndRate",
@ -1106,6 +1101,11 @@ export type MangoV4 = {
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
@ -1157,6 +1157,11 @@ export type MangoV4 = {
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
@ -1233,6 +1238,36 @@ export type MangoV4 = {
}
]
},
{
"name": "healthRegionBegin",
"accounts": [
{
"name": "instructions",
"isMut": false,
"isSigner": false,
"docs": [
"Instructions Sysvar for instruction introspection"
]
},
{
"name": "account",
"isMut": true,
"isSigner": false
}
],
"args": []
},
{
"name": "healthRegionEnd",
"accounts": [
{
"name": "account",
"isMut": true,
"isSigner": false
}
],
"args": []
},
{
"name": "serum3RegisterMarket",
"docs": [
@ -1285,6 +1320,30 @@ export type MangoV4 = {
]
}
},
{
"name": "indexReservation",
"isMut": true,
"isSigner": false,
"pda": {
"seeds": [
{
"kind": "const",
"type": "string",
"value": "Serum3Index"
},
{
"kind": "account",
"type": "publicKey",
"path": "group"
},
{
"kind": "arg",
"type": "u16",
"path": "market_index"
}
]
}
},
{
"name": "quoteBank",
"isMut": false,
@ -1546,24 +1605,20 @@ export type MangoV4 = {
]
},
{
"name": "quoteBank",
"name": "payerBank",
"isMut": true,
"isSigner": false
"isSigner": false,
"docs": [
"The bank that pays for the order, if necessary"
]
},
{
"name": "quoteVault",
"name": "payerVault",
"isMut": true,
"isSigner": false
},
{
"name": "baseBank",
"isMut": true,
"isSigner": false
},
{
"name": "baseVault",
"isMut": true,
"isSigner": false
"isSigner": false,
"docs": [
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "tokenProgram",
@ -3000,12 +3055,21 @@ export type MangoV4 = {
"",
"Normally accounts can not be liquidated while maint_health >= 0. But when an account",
"reaches maint_health < 0, liquidators will call a liquidation instruction and thereby",
"set this flag. Now the account may be liquidated until init_health >= 0."
"set this flag. Now the account may be liquidated until init_health >= 0.",
"",
"Many actions should be disabled while the account is being liquidated, even if",
"its maint health has recovered to positive. Creating new open orders would, for example,",
"confuse liquidators."
],
"type": "u8"
},
{
"name": "padding2",
"name": "inHealthRegion",
"docs": [
"The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.",
"",
"Must never be set after a transaction ends."
],
"type": "u8"
},
{
@ -3029,12 +3093,19 @@ export type MangoV4 = {
"name": "netSettled",
"type": "i64"
},
{
"name": "healthRegionPreInitHealth",
"docs": [
"Init health as calculated during HealthReginBegin, rounded up."
],
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
248
240
]
}
},
@ -3095,7 +3166,7 @@ export type MangoV4 = {
"name": "perpOpenOrders",
"type": {
"vec": {
"defined": "PerpOpenOrders"
"defined": "PerpOpenOrder"
}
}
}
@ -3789,6 +3860,10 @@ export type MangoV4 = {
{
"name": "quoteIndex",
"type": "u64"
},
{
"name": "marketIndex",
"type": "u16"
}
]
}
@ -3798,6 +3873,10 @@ export type MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "maintAssetWeight",
"type": {
@ -3932,11 +4011,17 @@ export type MangoV4 = {
"type": "publicKey"
},
{
"name": "previousNativeCoinReserved",
"name": "baseBorrowsWithoutFee",
"docs": [
"Tracks the amount of borrows that have flowed into the serum open orders account.",
"These borrows did not have the loan origination fee applied, and that may happen",
"later (in serum3_settle_funds) if we can guarantee that the funds were used.",
"In particular a place-on-book, cancel, settle should not cost fees."
],
"type": "u64"
},
{
"name": "previousNativePcReserved",
"name": "quoteBorrowsWithoutFee",
"type": "u64"
},
{
@ -4077,7 +4162,7 @@ export type MangoV4 = {
}
},
{
"name": "PerpOpenOrders",
"name": "PerpOpenOrder",
"type": {
"kind": "struct",
"fields": [
@ -5146,43 +5231,53 @@ export type MangoV4 = {
},
{
"code": 6007,
"name": "HealthMustBePositiveOrIncrease",
"msg": "health must be positive or increase"
},
{
"code": 6008,
"name": "HealthMustBeNegative",
"msg": "health must be negative"
},
{
"code": 6008,
"code": 6009,
"name": "IsBankrupt",
"msg": "the account is bankrupt"
},
{
"code": 6009,
"code": 6010,
"name": "IsNotBankrupt",
"msg": "the account is not bankrupt"
},
{
"code": 6010,
"code": 6011,
"name": "NoFreeTokenPositionIndex",
"msg": "no free token position index"
},
{
"code": 6011,
"code": 6012,
"name": "NoFreeSerum3OpenOrdersIndex",
"msg": "no free serum3 open orders index"
},
{
"code": 6012,
"code": 6013,
"name": "NoFreePerpPositionIndex",
"msg": "no free perp position index"
},
{
"code": 6013,
"code": 6014,
"name": "Serum3OpenOrdersExistAlready",
"msg": "serum3 open orders exist already"
},
{
"code": 6014,
"code": 6015,
"name": "InsufficentBankVaultFunds",
"msg": "bank vault has insufficent funds"
},
{
"code": 6016,
"name": "BeingLiquidated",
"msg": "account is currently being liquidated"
}
]
};
@ -5919,12 +6014,7 @@ export const IDL: MangoV4 = {
"isSigner": false
}
],
"args": [
{
"name": "tokenIndex",
"type": "u16"
}
]
"args": []
},
{
"name": "tokenUpdateIndexAndRate",
@ -6295,6 +6385,11 @@ export const IDL: MangoV4 = {
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
@ -6346,6 +6441,11 @@ export const IDL: MangoV4 = {
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
@ -6422,6 +6522,36 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "healthRegionBegin",
"accounts": [
{
"name": "instructions",
"isMut": false,
"isSigner": false,
"docs": [
"Instructions Sysvar for instruction introspection"
]
},
{
"name": "account",
"isMut": true,
"isSigner": false
}
],
"args": []
},
{
"name": "healthRegionEnd",
"accounts": [
{
"name": "account",
"isMut": true,
"isSigner": false
}
],
"args": []
},
{
"name": "serum3RegisterMarket",
"docs": [
@ -6474,6 +6604,30 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "indexReservation",
"isMut": true,
"isSigner": false,
"pda": {
"seeds": [
{
"kind": "const",
"type": "string",
"value": "Serum3Index"
},
{
"kind": "account",
"type": "publicKey",
"path": "group"
},
{
"kind": "arg",
"type": "u16",
"path": "market_index"
}
]
}
},
{
"name": "quoteBank",
"isMut": false,
@ -6735,24 +6889,20 @@ export const IDL: MangoV4 = {
]
},
{
"name": "quoteBank",
"name": "payerBank",
"isMut": true,
"isSigner": false
"isSigner": false,
"docs": [
"The bank that pays for the order, if necessary"
]
},
{
"name": "quoteVault",
"name": "payerVault",
"isMut": true,
"isSigner": false
},
{
"name": "baseBank",
"isMut": true,
"isSigner": false
},
{
"name": "baseVault",
"isMut": true,
"isSigner": false
"isSigner": false,
"docs": [
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "tokenProgram",
@ -8189,12 +8339,21 @@ export const IDL: MangoV4 = {
"",
"Normally accounts can not be liquidated while maint_health >= 0. But when an account",
"reaches maint_health < 0, liquidators will call a liquidation instruction and thereby",
"set this flag. Now the account may be liquidated until init_health >= 0."
"set this flag. Now the account may be liquidated until init_health >= 0.",
"",
"Many actions should be disabled while the account is being liquidated, even if",
"its maint health has recovered to positive. Creating new open orders would, for example,",
"confuse liquidators."
],
"type": "u8"
},
{
"name": "padding2",
"name": "inHealthRegion",
"docs": [
"The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.",
"",
"Must never be set after a transaction ends."
],
"type": "u8"
},
{
@ -8218,12 +8377,19 @@ export const IDL: MangoV4 = {
"name": "netSettled",
"type": "i64"
},
{
"name": "healthRegionPreInitHealth",
"docs": [
"Init health as calculated during HealthReginBegin, rounded up."
],
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
248
240
]
}
},
@ -8284,7 +8450,7 @@ export const IDL: MangoV4 = {
"name": "perpOpenOrders",
"type": {
"vec": {
"defined": "PerpOpenOrders"
"defined": "PerpOpenOrder"
}
}
}
@ -8978,6 +9144,10 @@ export const IDL: MangoV4 = {
{
"name": "quoteIndex",
"type": "u64"
},
{
"name": "marketIndex",
"type": "u16"
}
]
}
@ -8987,6 +9157,10 @@ export const IDL: MangoV4 = {
"type": {
"kind": "struct",
"fields": [
{
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "maintAssetWeight",
"type": {
@ -9121,11 +9295,17 @@ export const IDL: MangoV4 = {
"type": "publicKey"
},
{
"name": "previousNativeCoinReserved",
"name": "baseBorrowsWithoutFee",
"docs": [
"Tracks the amount of borrows that have flowed into the serum open orders account.",
"These borrows did not have the loan origination fee applied, and that may happen",
"later (in serum3_settle_funds) if we can guarantee that the funds were used.",
"In particular a place-on-book, cancel, settle should not cost fees."
],
"type": "u64"
},
{
"name": "previousNativePcReserved",
"name": "quoteBorrowsWithoutFee",
"type": "u64"
},
{
@ -9266,7 +9446,7 @@ export const IDL: MangoV4 = {
}
},
{
"name": "PerpOpenOrders",
"name": "PerpOpenOrder",
"type": {
"kind": "struct",
"fields": [
@ -10335,43 +10515,53 @@ export const IDL: MangoV4 = {
},
{
"code": 6007,
"name": "HealthMustBePositiveOrIncrease",
"msg": "health must be positive or increase"
},
{
"code": 6008,
"name": "HealthMustBeNegative",
"msg": "health must be negative"
},
{
"code": 6008,
"code": 6009,
"name": "IsBankrupt",
"msg": "the account is bankrupt"
},
{
"code": 6009,
"code": 6010,
"name": "IsNotBankrupt",
"msg": "the account is not bankrupt"
},
{
"code": 6010,
"code": 6011,
"name": "NoFreeTokenPositionIndex",
"msg": "no free token position index"
},
{
"code": 6011,
"code": 6012,
"name": "NoFreeSerum3OpenOrdersIndex",
"msg": "no free serum3 open orders index"
},
{
"code": 6012,
"code": 6013,
"name": "NoFreePerpPositionIndex",
"msg": "no free perp position index"
},
{
"code": 6013,
"code": 6014,
"name": "Serum3OpenOrdersExistAlready",
"msg": "serum3 open orders exist already"
},
{
"code": 6014,
"code": 6015,
"name": "InsufficentBankVaultFunds",
"msg": "bank vault has insufficent funds"
},
{
"code": 6016,
"name": "BeingLiquidated",
"msg": "account is currently being liquidated"
}
]
};

View File

@ -17,6 +17,8 @@ import { MANGO_V4_ID } from '../constants';
const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
['ETH/USDC', 'BkAraCyL9TTLbeMY3L1VWrPcv32DvSi5QDDQjik1J6Ac'],
['SRM/USDC', '249LDNPLLL29nRq8kjBTg9hKdXMcZf4vK2UvxszZYcuZ'],
]);
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
@ -24,12 +26,16 @@ const DEVNET_MINTS = new Map([
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
]);
const DEVNET_ORACLES = new Map([
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
@ -88,19 +94,19 @@ async function main() {
0.1,
0, // tokenIndex
'USDC',
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.0005,
0.8,
0.6,
1.2,
1.4,
0.02,
1,
1,
1,
1,
0,
);
await group.reloadAll(client);
} catch (error) {}
@ -117,19 +123,19 @@ async function main() {
0.1,
1, // tokenIndex
'BTC',
0.01,
0.4,
0.07,
0.8,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.88,
0.0005,
0.0005,
0.8,
0.6,
1.1,
1.2,
1.4,
0.02,
0.05,
);
await group.reloadAll(client);
} catch (error) {
@ -148,19 +154,19 @@ async function main() {
0.1,
2, // tokenIndex
'SOL',
0.01,
0.4,
0.07,
0.8,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.63,
0.0005,
0.0005,
0.8,
0.6,
1.1,
1.2,
1.4,
0.02,
0.05,
);
await group.reloadAll(client);
} catch (error) {
@ -198,14 +204,75 @@ async function main() {
console.log(error);
}
// register token 4
// register token 7
console.log(`Registering ETH...`);
const ethDevnetMint = new PublicKey(DEVNET_MINTS.get('ETH')!);
const ethDevnetOracle = new PublicKey(DEVNET_ORACLES.get('ETH')!);
try {
await client.tokenRegister(
group,
ethDevnetMint,
ethDevnetOracle,
0.1,
7, // tokenIndex
'ETH',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
// register token 5
console.log(`Registering SRM...`);
const srmDevnetMint = new PublicKey(DEVNET_MINTS.get('SRM')!);
const srmDevnetOracle = new PublicKey(DEVNET_ORACLES.get('SRM')!);
try {
await client.tokenRegister(
group,
srmDevnetMint,
srmDevnetOracle,
0.1,
5, // tokenIndex
'SRM',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
console.log(
`Editing group, setting existing admin as fastListingAdmin to be able to add MNGO truslessly...`,
);
let sig = await client.groupEdit(
group,
group.admin,
new PublicKey('Efhak3qj3MiyzgJr3cUUqXXz5wr3oYHt9sPzuqJf9eBN'),
group.admin,
undefined,
undefined,
);
@ -231,7 +298,7 @@ async function main() {
// register serum market
console.log(`Registering serum3 market...`);
const serumMarketExternalPk = new PublicKey(
let serumMarketExternalPk = new PublicKey(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
try {
@ -251,7 +318,35 @@ async function main() {
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
group.getFirstBankByMint(usdcDevnetMint).tokenIndex,
);
console.log(`...registerd serum3 market ${markets[0].publicKey}`);
console.log(`...registered serum3 market ${markets[0].publicKey}`);
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('ETH/USDC')!);
try {
await client.serum3RegisterMarket(
group,
serumMarketExternalPk,
group.getFirstBankByMint(ethDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
'ETH/USDC',
);
} catch (error) {
console.log(error);
}
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('SRM/USDC')!);
try {
await client.serum3RegisterMarket(
group,
serumMarketExternalPk,
group.getFirstBankByMint(srmDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
'SRM/USDC',
);
} catch (error) {
console.log(error);
}
// register perp market
console.log(`Registering perp market...`);
@ -292,114 +387,93 @@ async function main() {
// edit
//
console.log(`Editing USDC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
btcDevnetOracle,
0.1,
undefined,
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.getFirstBankByMint(btcDevnetMint).toString());
} catch (error) {
throw error;
}
console.log(`Resetting USDC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.0005,
0.0005,
1.0,
1.0,
1.0,
1.0,
0.02,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(usdcDevnetMint).toString());
} catch (error) {
throw error;
}
if (true) {
console.log(`Editing USDC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
1,
1,
1,
1,
0,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(btcDevnetMint).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);
console.log(`Editing BTC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
} catch (error) {
throw error;
}
console.log(`Editing SOL...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
} catch (error) {
throw error;
}
}
process.exit();

View File

@ -0,0 +1,136 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
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.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
// 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();
// note: see note above
// options.commitment = 'finalized';
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// user
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER2_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'],
{},
'get-program-accounts',
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
try {
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`Found group ${group.publicKey.toBase58()}`);
// fetch account
const mangoAccount = (
await client.getMangoAccountsForOwner(group, user.publicKey)
)[0];
console.log(`...found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// close mango account serum3 positions, closing might require cancelling orders and settling
for (const serum3Account of mangoAccount.serum3Active()) {
let orders = await client.getSerum3Orders(
group,
group.findSerum3Market(serum3Account.marketIndex)!.name,
);
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 client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
await client.serum3SettleFunds(
group,
mangoAccount,
group.findSerum3Market(serum3Account.marketIndex)!.name,
);
await client.serum3CloseOpenOrders(
group,
mangoAccount,
group.findSerum3Market(serum3Account.marketIndex)!.name,
);
}
// we closed a serum account, this changes the health accounts we are passing in for future ixs
await mangoAccount.reload(client, group);
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
let native = token.balance(
group.getFirstBankByTokenIndex(token.tokenIndex),
);
// to avoid rounding issues
if (native.toNumber() < 1) {
continue;
}
let nativeFlooredNumber = Math.floor(native.toNumber());
console.log(
`withdrawing token ${
group.getFirstBankByTokenIndex(token.tokenIndex).name
} native amount ${nativeFlooredNumber} `,
);
await client.tokenWithdrawNative(
group,
mangoAccount,
group.getFirstBankByTokenIndex(token.tokenIndex).mint,
nativeFlooredNumber - 1 /* see comment in token_withdraw in program */,
false,
);
}
// reload and print current positions
await mangoAccount.reload(client, group);
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// close account
console.log(`Close mango account...`);
const res = await client.closeMangoAccount(group, mangoAccount);
} catch (error) {
console.log(error);
}
process.exit();
}
main();

View File

@ -0,0 +1,134 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
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
//
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
]);
const DEVNET_ORACLES = new Map([
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
]);
export const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER2_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'],
{},
'get-program-accounts',
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`${group}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));
if (true) {
// deposit and withdraw
try {
console.log(`...depositing`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
1000,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
100,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('ETH')!),
500,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SRM')!),
500,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('BTC')!),
1,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString(group));
} catch (error) {
console.log(error);
}
}
process.exit();
}
main();

View File

@ -94,7 +94,7 @@ async function main() {
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
let native = token.native(
let native = token.balance(
group.getFirstBankByTokenIndex(token.tokenIndex),
);

View File

@ -1,6 +1,7 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { I80F48 } from '../accounts/I80F48';
import { HealthType } from '../accounts/mangoAccount';
import { OrderType, Side } from '../accounts/perp';
import {
@ -28,6 +29,10 @@ const DEVNET_MINTS = new Map([
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
export const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
@ -61,7 +66,7 @@ async function main() {
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
console.log(`${group}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
@ -70,9 +75,11 @@ async function main() {
user.publicKey,
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
console.log(mangoAccount.toString(group));
if (true) {
await mangoAccount.reload(client, group);
if (false) {
// set delegate, and change name
console.log(`...changing mango account name, and setting a delegate`);
const randomKey = new PublicKey(
@ -99,7 +106,8 @@ async function main() {
console.log(mangoAccount.toString());
}
if (true) {
if (false) {
// expand account
console.log(
`...expanding mango account to have serum3 and perp position slots`,
);
@ -107,11 +115,11 @@ async function main() {
await mangoAccount.reload(client, group);
}
if (true) {
if (false) {
// deposit and withdraw
try {
console.log(`...depositing 50 USDC`);
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
group,
mangoAccount,
@ -120,6 +128,22 @@ async function main() {
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client, group);
console.log(`...withdrawing 1 USDC`);
await client.tokenWithdraw(
group,
@ -138,21 +162,44 @@ async function main() {
0.0005,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString(group));
} catch (error) {
console.log(error);
}
}
if (false) {
// serum3
const serum3Market = group.serum3MarketsMapByExternal.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
);
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
);
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const lowestAsk = Array.from(asks!)[0];
const bids = await group.loadSerum3BidsForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
let price = 20;
let qty = 0.0001;
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 at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid,
20,
0.0001,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
@ -160,15 +207,18 @@ async function main() {
);
await mangoAccount.reload(client, group);
console.log(`...placing serum3 bid way above midprice`);
price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001;
console.log(
`...placing serum3 bid way above midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid,
90000,
0.0001,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
@ -176,15 +226,18 @@ async function main() {
);
await mangoAccount.reload(client, group);
console.log(`...placing serum3 ask way below midprice`);
price = highestBid.price - highestBid.price / 2;
qty = 0.0001;
console.log(
`...placing serum3 ask way below midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.ask,
30000,
0.0001,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
@ -192,10 +245,10 @@ async function main() {
);
console.log(`...current own orders on OB`);
let orders = await client.getSerum3Orders(
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
for (const order of orders) {
console.log(
@ -205,18 +258,17 @@ async function main() {
await client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
console.log(`...current own orders on OB`);
orders = await client.getSerum3Orders(
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
for (const order of orders) {
console.log(order);
@ -226,12 +278,19 @@ async function main() {
await client.serum3SettleFunds(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
}
if (true) {
if (false) {
// serum3 market
const serum3Market = group.serum3MarketsMapByExternal.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(),
);
console.log(await serum3Market?.logOb(client, group));
}
if (false) {
await mangoAccount.reload(client, group);
console.log(
'...mangoAccount.getEquity() ' +
@ -252,13 +311,13 @@ async function main() {
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsVal(HealthType.init).toNumber(),
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsVal(HealthType.init).toNumber(),
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
),
);
console.log(
@ -272,14 +331,80 @@ async function main() {
).toNumber(),
),
);
console.log(
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
toUiDecimalsForQuote(
mangoAccount
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
.toNumber(),
),
}
if (true) {
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const lowestAsk = Array.from(asks!)[0];
const bids = await group.loadSerum3BidsForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
function getMaxSourceForTokenSwapWrapper(src, tgt) {
// console.log();
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount
.getMaxSourceForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)
.div(
I80F48.fromNumber(
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
),
)
.toNumber(),
);
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
for (const tgtToken of Array.from(group.banksMapByName.keys())) {
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
}
}
const maxQuoteForSerum3BidUi = mangoAccount.getMaxQuoteForSerum3BidUi(
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
console.log(
"...mangoAccount.getMaxQuoteForSerum3BidUi(group, 'BTC/USDC') " +
maxQuoteForSerum3BidUi,
);
const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi(
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
console.log(
"...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " +
maxBaseForSerum3AskUi,
);
console.log(
`simHealthRatioWithSerum3BidUiChanges ${mangoAccount.simHealthRatioWithSerum3BidUiChanges(
group,
785,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
)}`,
);
console.log(
`simHealthRatioWithSerum3AskUiChanges ${mangoAccount.simHealthRatioWithSerum3AskUiChanges(
group,
0.033,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
)}`,
);
}
if (false) {
console.log(
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
toUiDecimalsForQuote(
@ -290,7 +415,7 @@ async function main() {
);
}
if (true) {
if (false) {
// perps
console.log(`...placing perp bid`);
try {

View File

@ -9,8 +9,11 @@ import {
TransactionInstruction,
} from '@solana/web3.js';
import BN from 'bn.js';
import { QUOTE_DECIMALS } from './accounts/bank';
import { Bank, QUOTE_DECIMALS } from './accounts/bank';
import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
import { PerpMarket } from './accounts/perp';
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
@ -26,6 +29,59 @@ export function debugAccountMetas(ams: AccountMeta[]) {
}
}
export function debugHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
publicKeys: PublicKey[],
) {
const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(),
`${banks[0].name} bank`,
]),
);
const oracles = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].oracle.toBase58(),
`${banks[0].name} oracle`,
]),
);
const serum3 = new Map(
mangoAccount.serum3Active().map((serum3: Serum3Orders) => {
const serum3Market = Array.from(
group.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex);
if (!serum3Market) {
throw new Error(
`Serum3Orders for non existent market with market index ${serum3.marketIndex}`,
);
}
return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`];
}),
);
const perps = new Map(
Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
]),
);
publicKeys.map((pk) => {
if (banks.get(pk.toBase58())) {
console.log(banks.get(pk.toBase58()));
}
if (oracles.get(pk.toBase58())) {
console.log(oracles.get(pk.toBase58()));
}
if (serum3.get(pk.toBase58())) {
console.log(serum3.get(pk.toBase58()));
}
if (perps.get(pk.toBase58())) {
console.log(perps.get(pk.toBase58()));
}
});
}
export async function findOrCreate<T>(
entityName: string,
findMethod: (...x: any) => any,