Merge branch 'main' into feature/modify-order

This commit is contained in:
Adrian Brzeziński 2022-12-14 16:58:55 +01:00
commit 8785d007bd
30 changed files with 944 additions and 752 deletions

View File

@ -21,7 +21,7 @@ on:
env:
CARGO_TERM_COLOR: always
SOLANA_VERSION: '1.13.3'
SOLANA_VERSION: '1.14.9'
RUST_TOOLCHAIN: '1.60.0'
LOG_PROGRAM: 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD'

3
.gitmodules vendored
View File

@ -2,3 +2,6 @@
path = anchor
url = https://github.com/blockworks-foundation/anchor.git
branch = v0.25.0-mangov4
[submodule "switchboard-v2"]
path = switchboard-v2
url = https://github.com/blockworks-foundation/sbv2-solana.git

1154
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,8 @@ members = [
[patch.crates-io]
# for gzip encoded responses
jsonrpc-core-client = { git = "https://github.com/ckamm/jsonrpc.git", branch = "ckamm/http-with-gzip" }
# these patch anchor to use solana 1.10.35 in order to fix dependency conflicts in switchboard-v2
# these patch anchor to use solana 1.14.9 in order to fix dependency conflicts in switchboard-v2
# downstream applications may need to copy these patches to their own workspace
anchor-spl = { path = "./anchor/spl/" }
anchor-lang = { path = "./anchor/lang/" }
switchboard-v2 = { path = "./switchboard-v2/rust/switchboard-v2/" }

2
anchor

@ -1 +1 @@
Subproject commit 3f8373770c51e105730b0bfd65208d3ba704c9ec
Subproject commit 309c2c2f4cce7c0a13d307fab3c7e2985bff3fa5

View File

@ -21,6 +21,6 @@ log = "0.4.0"
mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client"] }
pyth-sdk-solana = "0.1.0"
serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false,features = ["no-entrypoint", "program"], branch = "ckamm/find_by_key" }
solana-client = "~1.10.35"
solana-sdk = "~1.10.35"
solana-client = "~1.14.9"
solana-sdk = "~1.14.9"
tokio = { version = "1.14.1", features = ["rt-multi-thread", "time", "macros", "sync"] }

View File

@ -18,9 +18,9 @@ mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client
pyth-sdk-solana = "0.1.0"
serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false,features = ["no-entrypoint", "program"], branch = "ckamm/find_by_key" }
shellexpand = "2.1.0"
solana-account-decoder = "~1.10.35"
solana-client = "~1.10.35"
solana-sdk = "~1.10.35"
solana-account-decoder = "~1.14.9"
solana-client = "~1.14.9"
solana-sdk = "~1.14.9"
spl-associated-token-account = "1.0.3"
thiserror = "1.0.31"
log = "0.4"

View File

@ -65,15 +65,12 @@ impl AccountFetcher for RpcAccountFetcher {
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
use solana_account_decoder::UiAccountEncoding;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{
Memcmp, MemcmpEncodedBytes, MemcmpEncoding, RpcFilterType,
};
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
let config = RpcProgramAccountsConfig {
filters: Some(vec![RpcFilterType::Memcmp(Memcmp {
offset: 0,
bytes: MemcmpEncodedBytes::Bytes(discriminator.to_vec()),
encoding: Some(MemcmpEncoding::Binary),
})]),
filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
0,
discriminator.to_vec(),
))]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
commitment: Some(self.rpc.commitment()),

View File

@ -5,7 +5,7 @@ use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarke
use solana_account_decoder::UiAccountEncoding;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
use solana_sdk::pubkey::Pubkey;
pub fn fetch_mango_accounts(
@ -15,21 +15,12 @@ pub fn fetch_mango_accounts(
) -> Result<Vec<(Pubkey, MangoAccountValue)>, ClientError> {
let config = RpcProgramAccountsConfig {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 0,
bytes: MemcmpEncodedBytes::Bytes(MangoAccount::discriminator().to_vec()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(owner.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
0,
&MangoAccount::discriminator(),
)),
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(8, &group.to_bytes())),
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(40, &owner.to_bytes())),
]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
@ -46,42 +37,38 @@ pub fn fetch_mango_accounts(
}
pub fn fetch_banks(program: &Program, group: Pubkey) -> Result<Vec<(Pubkey, Bank)>, ClientError> {
program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
}
pub fn fetch_mint_infos(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, MintInfo)>, ClientError> {
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
}
pub fn fetch_serum3_markets(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, Serum3Market)>, ClientError> {
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
}
pub fn fetch_perp_markets(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, PerpMarket)>, ClientError> {
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
8,
&group.to_bytes(),
))])
}

View File

@ -19,3 +19,16 @@ done
# errors on enums that have tuple variants. This hack drops these from the idl.
perl -0777 -pi -e 's/ *{\s*"name": "NodeRef(?<nested>(?:[^{}[\]]+|\{(?&nested)\}|\[(?&nested)\])*)\},\n//g' \
target/idl/mango_v4.json target/types/mango_v4.ts;
# Reduce size of idl to be uploaded to chain
cp target/idl/mango_v4.json target/idl/mango_v4_no_docs.json
jq 'del(.types[]?.docs)' target/idl/mango_v4_no_docs.json \
> target/idl/mango_v4_no_docs.json.bak && mv target/idl/mango_v4_no_docs.json.bak target/idl/mango_v4_no_docs.json
jq 'del(.accounts[]?.type.fields[]?.docs)' target/idl/mango_v4_no_docs.json \
> target/idl/mango_v4_no_docs.json.bak && mv target/idl/mango_v4_no_docs.json.bak target/idl/mango_v4_no_docs.json
jq 'del(.instructions[]?.docs)' target/idl/mango_v4_no_docs.json \
> target/idl/mango_v4_no_docs.json.bak && mv target/idl/mango_v4_no_docs.json.bak target/idl/mango_v4_no_docs.json
jq 'del(.instructions[]?.accounts[]?.docs)' target/idl/mango_v4_no_docs.json \
> target/idl/mango_v4_no_docs.json.bak && mv target/idl/mango_v4_no_docs.json.bak target/idl/mango_v4_no_docs.json
jq 'del(.types[]?.type.fields[]?.docs)' target/idl/mango_v4_no_docs.json \
> target/idl/mango_v4_no_docs.json.bak && mv target/idl/mango_v4_no_docs.json.bak target/idl/mango_v4_no_docs.json

View File

@ -22,6 +22,6 @@ log = "0.4.0"
mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client"] }
pyth-sdk-solana = "0.1.0"
serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false,features = ["no-entrypoint", "program"], branch = "ckamm/find_by_key" }
solana-client = "~1.10.35"
solana-sdk = "~1.10.35"
solana-client = "~1.14.9"
solana-sdk = "~1.14.9"
tokio = { version = "1.14.1", features = ["rt-multi-thread", "time", "macros", "sync"] }

View File

@ -36,11 +36,11 @@ serde_derive = "1.0.130"
serde_json = "1.0.68"
serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false,features = ["no-entrypoint", "program"], branch = "ckamm/find_by_key" }
shellexpand = "2.1.0"
solana-account-decoder = "~1.10.35"
solana-client = "~1.10.35"
solana-logger = "~1.10.35"
solana-rpc = "~1.10.35"
solana-sdk = "~1.10.35"
solana-account-decoder = "~1.14.9"
solana-client = "~1.14.9"
solana-logger = "~1.14.9"
solana-rpc = "~1.14.9"
solana-sdk = "~1.14.9"
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.16.1"

View File

@ -3,12 +3,9 @@ use jsonrpc_core_client::transports::http;
use solana_account_decoder::{UiAccount, UiAccountEncoding};
use solana_client::{
rpc_config::{RpcAccountInfoConfig, RpcContextConfig, RpcProgramAccountsConfig},
rpc_response::{Response, RpcKeyedAccount},
};
use solana_rpc::{
rpc::OptionalContext,
rpc::{rpc_accounts::AccountsDataClient, rpc_minimal::MinimalClient},
rpc_response::{OptionalContext, Response, RpcKeyedAccount},
};
use solana_rpc::rpc::{rpc_accounts::AccountsDataClient, rpc_minimal::MinimalClient};
use solana_sdk::{account::AccountSharedData, commitment_config::CommitmentConfig, pubkey::Pubkey};
use anyhow::Context;

View File

@ -4,7 +4,7 @@ use jsonrpc_core_client::transports::ws;
use solana_account_decoder::UiAccountEncoding;
use solana_client::{
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
rpc_filter::{Memcmp, RpcFilterType},
rpc_response::{Response, RpcKeyedAccount, RpcResponseContext},
};
use solana_rpc::rpc_pubsub::RpcSolPubSubClient;
@ -78,17 +78,16 @@ async fn feed_data(
// filter for only OpenOrders with v4 authority
filters: Some(vec![
RpcFilterType::DataSize(3228), // open orders size
RpcFilterType::Memcmp(Memcmp {
offset: 0,
// "serum" + u64 that is Initialized (1) + OpenOrders (4)
bytes: MemcmpEncodedBytes::Base58("AcUQf4PGf6fCHGwmpB".into()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 45, // owner is the 4th field, after "serum" (header), account_flags: u64 and market: Pubkey
bytes: MemcmpEncodedBytes::Bytes(config.open_orders_authority.to_bytes().into()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
// new_base58_encoded() does not work with old RPC nodes
0,
[0x73, 0x65, 0x72, 0x75, 0x6d, 5, 0, 0, 0, 0, 0, 0, 0].to_vec(),
)),
RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
45,
config.open_orders_authority.to_bytes().to_vec(),
)),
]),
with_context: Some(true),
account_config: account_info_config.clone(),

View File

@ -19,7 +19,7 @@
"build": "npm run build:esm; npm run build:cjs",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 10000",
"test": "ts-mocha ts/client/**/*.spec.ts --timeout 15000",
"clean": "rm -rf dist",
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",

View File

@ -35,18 +35,17 @@ num_enum = "0.5.1"
pyth-sdk-solana = "0.1.0"
serde = "^1.0"
serum_dex = { version = "0.5.6", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false,features = ["no-entrypoint", "program"], branch = "ckamm/find_by_key" }
solana-address-lookup-table-program = "~1.10.35"
solana-program = "~1.10.35"
solana-sdk = { version = "~1.10.35", default-features = false, optional = true }
solana-address-lookup-table-program = "~1.14.9"
solana-program = "~1.14.9"
solana-sdk = { version = "~1.14.9", default-features = false, optional = true }
static_assertions = "1.1"
switchboard-program = ">=0.2.0"
switchboard-utils = ">=0.1.36"
switchboard-v2 = "0.1.12"
switchboard-v2 = "0.1.17"
[dev-dependencies]
solana-sdk = { version = "~1.10.35", default-features = false }
solana-program-test = "~1.10.35"
solana-logger = "~1.10.35"
solana-sdk = { version = "~1.14.9", default-features = false }
solana-program-test = "~1.14.9"
solana-logger = "~1.14.9"
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^1.0.3", features = ["no-entrypoint"] }
bincode = "^1.3.1"

View File

@ -15,7 +15,7 @@ use super::*;
const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001);
/// Information about prices for a bank or perp market.
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct Prices {
/// The current oracle price
pub oracle: I80F48, // native/native
@ -87,7 +87,7 @@ pub fn compute_health(
Ok(new_health_cache(account, retriever)?.health(health_type))
}
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct TokenInfo {
pub token_index: TokenIndex,
pub maint_asset_weight: I80F48,
@ -129,7 +129,7 @@ impl TokenInfo {
}
}
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct Serum3Info {
// reserved amounts as stored on the open orders
pub reserved_base: I80F48,
@ -208,7 +208,7 @@ struct Serum3Reserved {
all_reserved_as_quote: I80F48,
}
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct PerpInfo {
pub perp_market_index: PerpMarketIndex,
pub maint_asset_weight: I80F48,
@ -313,7 +313,7 @@ impl PerpInfo {
}
}
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct HealthCache {
pub(crate) token_infos: Vec<TokenInfo>,
pub(crate) serum3_infos: Vec<Serum3Info>,

View File

@ -206,18 +206,36 @@ impl HealthCache {
// - source_liab_weight * source_liab_price * a
// + target_asset_weight * target_asset_price * price * a = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
if point1_health <= 0 {
return Ok(I80F48::ZERO);
}
let zero_health_amount = point1_amount - point1_health / final_health_slope;
let zero_health_estimate = point1_amount - point1_health / final_health_slope;
let right_bound = scan_right_until_less_than(
zero_health_estimate,
min_ratio,
health_ratio_after_swap,
)?;
if right_bound == zero_health_estimate {
binary_search(
point1_amount,
point1_ratio,
zero_health_amount,
right_bound,
min_ratio,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
} else {
binary_search(
zero_health_estimate,
health_ratio_after_swap(zero_health_estimate)?,
right_bound,
min_ratio,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
}
} else if point0_ratio >= min_ratio {
// Must be between point0_amount and point1_amount.
binary_search(
@ -373,6 +391,25 @@ impl HealthCache {
}
}
fn scan_right_until_less_than(
start: I80F48,
target: I80F48,
fun: impl Fn(I80F48) -> Result<I80F48>,
) -> Result<I80F48> {
let max_iterations = 20;
let mut current = start;
for _ in 0..max_iterations {
let value = fun(current)?;
if value <= target {
return Ok(current);
}
current = current.max(I80F48::ONE) * I80F48::from(2);
}
Err(error_msg!(
"could not find amount that lead to health ratio <= 0"
))
}
fn binary_search(
mut left: I80F48,
left_value: I80F48,
@ -536,12 +573,11 @@ mod tests {
.map(|c| c.health_ratio(HealthType::Init).to_num::<f64>())
.unwrap_or(f64::MIN)
};
// With the binary search error, we can guarantee just +-1
(
source_amount.to_num(),
ratio_for_amount(source_amount),
ratio_for_amount(source_amount.saturating_sub(I80F48::ONE)),
ratio_for_amount(source_amount.saturating_add(I80F48::ONE)),
ratio_for_amount(source_amount - I80F48::ONE),
ratio_for_amount(source_amount + I80F48::ONE),
)
};
let check_max_swap_result = |c: &HealthCache,
@ -556,8 +592,9 @@ mod tests {
"checking {source} to {target} for price_factor: {price_factor}, target ratio {ratio}: actual ratios: {minus_ratio}/{actual_ratio}/{plus_ratio}, amount: {source_amount}",
);
assert!(actual_ratio >= ratio);
assert!(minus_ratio < ratio || actual_ratio < minus_ratio);
assert!(plus_ratio < ratio);
// either we're within tolerance of the target, or swapping 1 more would
// bring us below the target
assert!(actual_ratio < ratio + 1.0 || plus_ratio < ratio);
};
{
@ -654,6 +691,34 @@ mod tests {
// (tracking happens without decimals)
assert!(find_max_swap_actual(&health_cache, 0, 1, 1.0, 1.0, banks).0 < 51.0);
}
{
// check with serum reserved
println!("test 6");
let mut health_cache = health_cache.clone();
health_cache.serum3_infos = vec![Serum3Info {
base_index: 1,
quote_index: 0,
market_index: 0,
reserved_base: I80F48::from(30 / 3),
reserved_quote: I80F48::from(30 / 2),
}];
adjust_by_usdc(&mut health_cache, 0, -20.0);
adjust_by_usdc(&mut health_cache, 1, -40.0);
adjust_by_usdc(&mut health_cache, 2, 120.0);
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 2, target, price_factor, banks);
check_max_swap_result(&health_cache, 2, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 2, 1, target, price_factor, banks);
}
}
}
}
#[test]

View File

@ -20,4 +20,4 @@ test-bpf = []
[dependencies]
anchor-lang = { path = "../../anchor/lang" }
anchor-spl = { path = "../../anchor/spl" }
solana-program = "~1.10.35"
solana-program = "~1.14.9"

View File

@ -22,7 +22,7 @@ solana --url https://mango.devnet.rpcpool.com program deploy --program-id $PROGR
# # publish idl
cargo run -p anchor-cli -- idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \
--filepath target/idl/mango_v4.json $PROGRAM_ID
--filepath target/idl/mango_v4_no_docs.json $PROGRAM_ID
# build npm package

View File

@ -22,7 +22,7 @@ solana --url $MB_CLUSTER_URL program deploy --program-id $PROGRAM_ID \
# publish idl
cargo run -p anchor-cli -- idl upgrade --provider.cluster $MB_CLUSTER_URL --provider.wallet $WALLET_WITH_FUNDS \
--filepath target/idl/mango_v4.json $PROGRAM_ID
--filepath target/idl/mango_v4_no_docs.json $PROGRAM_ID
# build npm package

1
switchboard-v2 Submodule

@ -0,0 +1 @@
Subproject commit 97ed7b12de198f9dca2637af0ce8f5823cc3a0e3

View File

@ -72,7 +72,6 @@ export class Bank implements BankForHealth {
static from(
publicKey: PublicKey,
obj: {
// TODO: rearrange fields to have same order as in bank.rs
group: PublicKey;
name: number[];
mint: PublicKey;
@ -198,7 +197,7 @@ export class Bank implements BankForHealth {
public tokenIndex: TokenIndex,
public mintDecimals: number,
public bankNum: number,
minVaultToDepositsRatio: number,
public minVaultToDepositsRatio: number,
netBorrowLimitWindowSizeTs: BN,
lastNetBorrowsWindowStartTs: BN,
public netBorrowLimitPerWindowQuote: BN,

View File

@ -500,7 +500,6 @@ export class Group {
}
public getSerum3FeeRates(maker = true): number {
// TODO: fetch msrm/srm vault balance
const feeTier = getFeeTier(0, 0);
const rates = getFeeRates(feeTier);
return maker ? rates.maker : rates.taker;

View File

@ -14,6 +14,7 @@ function mockBankAndOracle(
maintWeight: number,
initWeight: number,
price: number,
stablePrice: number,
): BankForHealth {
return {
tokenIndex,
@ -22,7 +23,7 @@ function mockBankAndOracle(
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: price } as StablePriceModel,
stablePriceModel: { stablePrice: stablePrice } as StablePriceModel,
scaledInitAssetWeight: () => I80F48.fromNumber(1 - initWeight),
scaledInitLiabWeight: () => I80F48.fromNumber(1 + initWeight),
};
@ -57,12 +58,14 @@ describe('Health Cache', () => {
0.1,
0.2,
1,
1,
);
const targetBank: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
5,
);
const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100));
@ -146,18 +149,21 @@ describe('Health Cache', () => {
0.1,
0.2,
1,
1,
);
const bank2: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
5,
);
const bank3: BankForHealth = mockBankAndOracle(
5 as TokenIndex,
0.3,
0.5,
10,
10,
);
const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1));
@ -390,9 +396,9 @@ describe('Health Cache', () => {
});
it('test_max_swap', (done) => {
const b0 = mockBankAndOracle(0 as TokenIndex, 0.1, 0.1, 2);
const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 3);
const b2 = mockBankAndOracle(2 as TokenIndex, 0.3, 0.3, 4);
const b0 = mockBankAndOracle(0 as TokenIndex, 0.1, 0.1, 2, 2);
const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 3, 3);
const b2 = mockBankAndOracle(2 as TokenIndex, 0.3, 0.3, 4, 4);
const banks = [b0, b1, b2];
const hc = new HealthCache(
[
@ -475,13 +481,13 @@ describe('Health Cache', () => {
for (const priceFactor of [0.1, 0.9, 1.1]) {
for (const target of _.range(1, 100, 1)) {
// checkMaxSwapResult(
// clonedHc,
// 0 as TokenIndex,
// 1 as TokenIndex,
// target,
// priceFactor,
// );
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
@ -489,13 +495,13 @@ describe('Health Cache', () => {
target,
priceFactor,
);
// checkMaxSwapResult(
// clonedHc,
// 0 as TokenIndex,
// 2 as TokenIndex,
// target,
// priceFactor,
// );
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
}
}
@ -623,12 +629,90 @@ describe('Health Cache', () => {
checkMaxSwapResult(clonedHc, 0 as TokenIndex, 1 as TokenIndex, 4, 1);
}
// TODO test 5
{
console.log(' - test 6');
const clonedHc = _.cloneDeep(hc);
clonedHc.serum3Infos = [
new Serum3Info(
I80F48.fromNumber(30 / 3),
I80F48.fromNumber(30 / 2),
1,
0,
0 as MarketIndex,
),
];
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
);
for (const priceFactor of [
// 0.9,
1.1,
]) {
for (const target of _.range(1, 100, 1)) {
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
0 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
1 as TokenIndex,
2 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
0 as TokenIndex,
target,
priceFactor,
);
checkMaxSwapResult(
clonedHc,
2 as TokenIndex,
1 as TokenIndex,
target,
priceFactor,
);
}
}
}
done();
});
it('test_max_perp', (done) => {
const baseLotSize = 100;
const b0 = mockBankAndOracle(0 as TokenIndex, 0.0, 0.0, 1);
const b0 = mockBankAndOracle(0 as TokenIndex, 0.0, 0.0, 1, 1);
const p0 = mockPerpMarket(0, 0.3, 0.3, baseLotSize, 2);
const hc = new HealthCache(
[TokenInfo.fromBank(b0, I80F48.fromNumber(0))],

View File

@ -548,26 +548,47 @@ export class HealthCache {
);
}
private static scanRightUntilLessThan(
start: I80F48,
target: I80F48,
fun: (amount: I80F48) => I80F48,
): I80F48 {
const maxIterations = 100;
let current = start;
// console.log(`scanRightUntilLessThan, start ${start.toLocaleString()}`);
for (const key of Array(maxIterations).fill(0).keys()) {
const value = fun(current);
if (value.lt(target)) {
return current;
}
// console.log(
// ` - current ${current.toLocaleString()}, value ${value.toLocaleString()}, target ${target.toLocaleString()}`,
// );
current = current.max(ONE_I80F48()).mul(I80F48.fromNumber(2));
}
throw new Error('Could not find amount that led to health ratio <=0');
}
private static binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
leftValue: I80F48,
right: I80F48,
rightRatio: I80F48,
targetRatio: I80F48,
targetValue: I80F48,
minStep: I80F48,
healthRatioAfterActionFn: (I80F48) => I80F48,
fun: (I80F48) => I80F48,
): I80F48 {
const maxIterations = 20;
const targetError = I80F48.fromNumber(0.1);
const rightValue = fun(right);
if (
(leftRatio.sub(targetRatio).isPos() &&
rightRatio.sub(targetRatio).isPos()) ||
(leftRatio.sub(targetRatio).isNeg() &&
rightRatio.sub(targetRatio).isNeg())
(leftValue.sub(targetValue).isPos() &&
rightValue.sub(targetValue).isPos()) ||
(leftValue.sub(targetValue).isNeg() &&
rightValue.sub(targetValue).isNeg())
) {
throw new Error(
`Internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}, likely reason is the zeroAmount not been tight enough!`,
`Internal error: left ${leftValue.toNumber()} and right ${rightValue.toNumber()} don't contain the target value ${targetValue.toNumber()}!`,
);
}
@ -578,25 +599,16 @@ export class HealthCache {
return left;
}
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = healthRatioAfterActionFn(newAmount);
const error = newAmountRatio.sub(targetRatio);
const newAmountRatio = fun(newAmount);
const error = newAmountRatio.sub(targetValue);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
if (newAmountRatio.gt(targetValue) != rightValue.gt(targetValue)) {
left = newAmount;
leftRatio = newAmountRatio;
} else {
right = newAmount;
rightRatio = newAmountRatio;
}
// console.log(
// ` -- ${left.toNumber().toFixed(3)} (${leftRatio
// .toNumber()
// .toFixed(3)}) ${right.toNumber().toFixed(3)} (${rightRatio
// .toNumber()
// .toFixed(3)})`,
// );
}
console.error(
@ -726,31 +738,63 @@ export class HealthCache {
// 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 * priceFactor = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// reserved contributions offset, moving the actual zero point further to the right.
if (point1Health.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
const zeroHealthAmount = point1Amount.sub(
point1Health.div(finalHealthSlope),
const zeroHealthEstimate = point1Amount.sub(
point1Health.sub(finalHealthSlope),
);
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
const zeroHealth = healthAfterSwap(zeroHealthAmount);
const zeroHealthEstimateRatio = healthRatioAfterSwap(zeroHealthEstimate);
// console.log(`getMaxSourceForTokenSwap`);
// console.log(` - finalHealthSlope ${finalHealthSlope.toLocaleString()}`);
// console.log(` - minRatio ${minRatio.toLocaleString()}`);
// console.log(` - point0Amount ${point0Amount.toLocaleString()}`);
// console.log(` - point0Health ${point0Health.toLocaleString()}`);
// console.log(` - point0Ratio ${point0Ratio.toLocaleString()}`);
// console.log(` - point1Amount ${point1Amount.toLocaleString()}`);
// console.log(` - point1Health ${point1Health.toLocaleString()}`);
// console.log(` - point1Ratio ${point1Ratio.toLocaleString()}`);
// console.log(
// ` - zeroHealthEstimate ${zeroHealthEstimate.toLocaleString()}`,
// );
// console.log(
// ` - zeroHealthEstimateRatio ${zeroHealthEstimateRatio.toLocaleString()}`,
// );
const rightBound = HealthCache.scanRightUntilLessThan(
zeroHealthEstimate,
minRatio,
healthRatioAfterSwap,
);
if (rightBound.eq(zeroHealthEstimate)) {
amount = HealthCache.binaryApproximationSearch(
point1Amount,
point1Ratio,
zeroHealthAmount,
zeroHealthRatio,
rightBound,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
);
} else {
amount = HealthCache.binaryApproximationSearch(
zeroHealthEstimate,
healthRatioAfterSwap(zeroHealthEstimate),
rightBound,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
);
}
} else if (point0Ratio.gte(minRatio)) {
// Must be between point0Amount and point1Amount.
amount = HealthCache.binaryApproximationSearch(
point0Amount,
point0Ratio,
point1Amount,
point1Ratio,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
@ -761,7 +805,6 @@ export class HealthCache {
ZERO_I80F48(),
initialRatio,
point0Amount,
point0Ratio,
minRatio,
ZERO_I80F48(),
healthRatioAfterSwap,
@ -880,7 +923,6 @@ export class HealthCache {
initialAmount,
initialRatio,
zeroAmount,
zeroAmountRatio,
minRatio,
ONE_I80F48(),
healthRatioAfterPlacingOrder,
@ -994,7 +1036,6 @@ export class HealthCache {
case1Start,
case1StartRatio,
zeroHealthAmount,
zeroHealthRatio,
minRatio,
ONE_I80F48(),
healthRatioAfterTradeTrunc,
@ -1005,7 +1046,6 @@ export class HealthCache {
ZERO_I80F48(),
initialRatio,
case1Start,
case1StartRatio,
minRatio,
ONE_I80F48(),
healthRatioAfterTradeTrunc,

View File

@ -462,12 +462,33 @@ export class MangoAccount {
const initHealthWithoutExistingPosition = initHealth.sub(
existingPositionHealthContrib,
);
const maxBorrowNative = initHealthWithoutExistingPosition
let maxBorrowNative = initHealthWithoutExistingPosition
.div(tokenBank.initLiabWeight)
.div(tokenBank.price);
// Cap maxBorrow to maintain minVaultToDepositsRatio on the bank
const vaultAmount = group.vaultAmountsMap.get(tokenBank.vault.toBase58());
if (!vaultAmount) {
throw new Error(
`No vault amount found for ${tokenBank.name} vault ${tokenBank.vault}!`,
);
}
const vaultAmountAfterWithdrawingDeposits = I80F48.fromU64(vaultAmount).sub(
existingTokenDeposits,
);
const expectedVaultMinAmount = tokenBank
.nativeDeposits()
.mul(I80F48.fromNumber(tokenBank.minVaultToDepositsRatio));
if (vaultAmountAfterWithdrawingDeposits.gt(expectedVaultMinAmount)) {
maxBorrowNative = maxBorrowNative.min(
vaultAmountAfterWithdrawingDeposits.sub(expectedVaultMinAmount),
);
}
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
ONE_I80F48().add(tokenBank.loanOriginationFeeRate),
);
return maxBorrowNativeWithoutFees.add(existingTokenDeposits);
}

View File

@ -69,7 +69,6 @@ export type MangoClientOptions = {
prioritizationFee?: number;
};
// TODO: replace ui values with native as input wherever possible
export class MangoClient {
private idsSource: IdsSource;
private postSendTxCallback?: ({ txid }) => void;
@ -2239,7 +2238,6 @@ export class MangoClient {
group: Group,
mintPk: PublicKey,
): Promise<TransactionSignature> {
// TODO: handle updating multiple banks
const bank = group.getFirstBankByMint(mintPk);
const mintInfo = group.mintInfosMapByMint.get(mintPk.toString())!;
@ -2444,9 +2442,6 @@ export class MangoClient {
provider: Provider,
groupName: string,
): MangoClient {
// TODO: use IDL on chain or in repository? decide...
// Alternatively we could fetch IDL from chain.
// const idl = await Program.fetchIdl(MANGO_V4_ID, provider);
const idl = IDL;
const id = Id.fromIdsByName(groupName);

View File

@ -97,6 +97,9 @@ async function debugUser(
}
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
// Turn on for debugging specific pairs
// if (src != 'DAI' || tgt != 'ETH') return;
const maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,

View File

@ -126,7 +126,6 @@ export async function buildVersionedTx(
addressLookupTableAccounts: alts,
});
const vTx = new VersionedTransaction(message);
// TODO: remove use of any when possible in future
vTx.sign([
((provider as AnchorProvider).wallet as any).payer as Signer,
...additionalSigners,