Merge remote-tracking branch 'origin/deploy' into dev

This commit is contained in:
Christian Kamm 2024-03-14 11:26:33 +01:00
commit 6f85dfa42d
21 changed files with 1119 additions and 563 deletions

View File

@ -113,6 +113,14 @@ pub struct Cli {
#[clap(long, env, value_parser, value_delimiter = ',')]
pub(crate) rebalance_skip_tokens: Option<Vec<u16>>,
/// query jupiter for direct routes to and from these tokens
///
/// These alternate routes will only be used when the main USDC-based one does not
/// work or does not fit in a transaction. A direct route to/from USDC is always also an alternative.
/// The alternate route with the lowest price impact will be used.
#[clap(long, env, value_parser, value_delimiter = ',')]
pub(crate) rebalance_alternate_jupiter_route_tokens: Option<Vec<u16>>,
/// When closing borrows, the rebalancer can't close token positions exactly.
/// Instead it purchases too much and then gets rid of the excess in a second step.
/// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token.

View File

@ -242,9 +242,13 @@ async fn main() -> anyhow::Result<()> {
borrow_settle_excess: (1f64 + cli.rebalance_borrow_settle_excess).max(1f64),
refresh_timeout: Duration::from_secs(cli.rebalance_refresh_timeout_secs),
jupiter_version: cli.jupiter_version.into(),
skip_tokens: cli.rebalance_skip_tokens.unwrap_or(Vec::new()),
skip_tokens: cli.rebalance_skip_tokens.unwrap_or_default(),
alternate_jupiter_route_tokens: cli
.rebalance_alternate_jupiter_route_tokens
.unwrap_or_default(),
allow_withdraws: signer_is_owner,
};
rebalance_config.validate(&mango_client.context);
let rebalancer = Arc::new(rebalance::Rebalancer {
mango_client: mango_client.clone(),

View File

@ -5,7 +5,7 @@ use mango_v4::state::{
PlaceOrderType, Side, TokenIndex, QUOTE_TOKEN_INDEX,
};
use mango_v4_client::{
chain_data, jupiter, perp_pnl, MangoClient, PerpMarketContext, TokenContext,
chain_data, jupiter, perp_pnl, MangoClient, MangoGroupContext, PerpMarketContext, TokenContext,
TransactionBuilder, TransactionSize,
};
@ -28,9 +28,22 @@ pub struct Config {
pub refresh_timeout: Duration,
pub jupiter_version: jupiter::Version,
pub skip_tokens: Vec<TokenIndex>,
pub alternate_jupiter_route_tokens: Vec<TokenIndex>,
pub allow_withdraws: bool,
}
impl Config {
// panics on failure
pub fn validate(&self, context: &MangoGroupContext) {
self.skip_tokens.iter().for_each(|&ti| {
context.token(ti);
});
self.alternate_jupiter_route_tokens.iter().for_each(|&ti| {
context.token(ti);
});
}
}
fn token_bank(
token: &TokenContext,
account_fetcher: &chain_data::AccountFetcher,
@ -106,7 +119,7 @@ impl Rebalancer {
/// Grab three possible routes:
/// 1. USDC -> output (complex routes)
/// 2. USDC -> output (direct route only)
/// 3. SOL -> output (direct route only)
/// 3. alternate_jupiter_route_tokens -> output (direct route only)
/// Use 1. if it fits into a tx. Otherwise use the better of 2./3.
async fn token_swap_buy(
&self,
@ -114,16 +127,7 @@ impl Rebalancer {
in_amount_quote: u64,
) -> anyhow::Result<(Signature, jupiter::Quote)> {
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
let sol_token = self.mango_client.context.token(
*self
.mango_client
.context
.token_indexes_by_name
.get("SOL") // TODO: better use mint
.unwrap(),
);
let quote_mint = quote_token.mint;
let sol_mint = sol_token.mint;
let jupiter_version = self.config.jupiter_version;
let full_route_job = self.jupiter_quote(
@ -140,18 +144,21 @@ impl Rebalancer {
true,
jupiter_version,
);
let mut jobs = vec![full_route_job, direct_quote_route_job];
// For the SOL -> output route we need to adjust the in amount by the SOL price
let sol_price = self
.account_fetcher
.fetch_bank_price(&sol_token.first_bank())?;
let in_amount_sol = (I80F48::from(in_amount_quote) / sol_price)
.ceil()
.to_num::<u64>();
let direct_sol_route_job =
self.jupiter_quote(sol_mint, output_mint, in_amount_sol, true, jupiter_version);
let jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
for in_token_index in &self.config.alternate_jupiter_route_tokens {
let in_token = self.mango_client.context.token(*in_token_index);
// For the alternate output routes we need to adjust the in amount by the token price
let in_price = self
.account_fetcher
.fetch_bank_price(&in_token.first_bank())?;
let in_amount = (I80F48::from(in_amount_quote) / in_price)
.ceil()
.to_num::<u64>();
let direct_route_job =
self.jupiter_quote(in_token.mint, output_mint, in_amount, true, jupiter_version);
jobs.push(direct_route_job);
}
let mut results = futures::future::join_all(jobs).await;
let full_route = results.remove(0)?;
@ -173,7 +180,7 @@ impl Rebalancer {
/// Grab three possible routes:
/// 1. input -> USDC (complex routes)
/// 2. input -> USDC (direct route only)
/// 3. input -> SOL (direct route only)
/// 3. input -> alternate_jupiter_route_tokens (direct route only)
/// Use 1. if it fits into a tx. Otherwise use the better of 2./3.
async fn token_swap_sell(
&self,
@ -181,26 +188,21 @@ impl Rebalancer {
in_amount: u64,
) -> anyhow::Result<(Signature, jupiter::Quote)> {
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
let sol_token = self.mango_client.context.token(
*self
.mango_client
.context
.token_indexes_by_name
.get("SOL") // TODO: better use mint
.unwrap(),
);
let quote_mint = quote_token.mint;
let sol_mint = sol_token.mint;
let jupiter_version = self.config.jupiter_version;
let full_route_job =
self.jupiter_quote(input_mint, quote_mint, in_amount, false, jupiter_version);
let direct_quote_route_job =
self.jupiter_quote(input_mint, quote_mint, in_amount, true, jupiter_version);
let direct_sol_route_job =
self.jupiter_quote(input_mint, sol_mint, in_amount, true, jupiter_version);
let mut jobs = vec![full_route_job, direct_quote_route_job];
let jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
for out_token_index in &self.config.alternate_jupiter_route_tokens {
let out_token = self.mango_client.context.token(*out_token_index);
let direct_route_job =
self.jupiter_quote(input_mint, out_token.mint, in_amount, true, jupiter_version);
jobs.push(direct_route_job);
}
let mut results = futures::future::join_all(jobs).await;
let full_route = results.remove(0)?;

View File

@ -1,6 +1,6 @@
{
"name": "@blockworks-foundation/mango-v4",
"version": "0.21.31",
"version": "0.23.1",
"description": "Typescript Client for mango-v4 program.",
"repository": "https://github.com/blockworks-foundation/mango-v4",
"author": {

View File

@ -32,7 +32,11 @@ borsh = { version = "0.10.3", features = ["const-generics"] }
bytemuck = { version = "^1.7.2", features = ["min_const_generics"] }
default-env = "0.1.1"
derivative = "2.2.0"
fixed = { workspace = true, features = ["serde", "borsh", "debug-assert-in-release"] }
fixed = { workspace = true, features = [
"serde",
"borsh",
"debug-assert-in-release",
] }
num_enum = "0.5.1"
pyth-sdk-solana = { workspace = true }
serde = "^1.0"
@ -48,7 +52,9 @@ switchboard-program = "0.2"
switchboard-v2 = { package = "switchboard-solana", version = "0.28" }
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = ["no-entrypoint"] }
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = [
"no-entrypoint",
] }
[dev-dependencies]
@ -56,7 +62,9 @@ solana-sdk = { workspace = true, default-features = false }
solana-program-test = { workspace = true }
solana-logger = { workspace = true }
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^1.0.3", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^1.0.3", features = [
"no-entrypoint",
] }
bincode = "^1.3.1"
log = "0.4.14"
env_logger = "0.9.0"
@ -69,4 +77,4 @@ num = "0.4.0"
regex = "1"
serde_json = "1"
bs58 = "0.5"
anyhow = "1"
anyhow = "1"

View File

@ -73,6 +73,7 @@ async function main(): Promise<void> {
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
PublicKey.default,
0, // tokenIndex
'USDC',
{
@ -101,6 +102,7 @@ async function main(): Promise<void> {
group,
solDevnetMint,
solDevnetOracle,
PublicKey.default,
4, // tokenIndex
'SOL',
{
@ -130,6 +132,7 @@ async function main(): Promise<void> {
group,
usdtDevnetMint,
usdcDevnetOracle.publicKey,
PublicKey.default,
5, // tokenIndex
'USDT',
{
@ -163,6 +166,7 @@ async function main(): Promise<void> {
group.getFirstBankByMint(insuranceMint),
0,
'SOL/USDC',
0,
);
await group.reloadAll(client);
const serum3Market = group.getSerum3MarketByExternalMarket(
@ -211,6 +215,7 @@ async function main(): Promise<void> {
1.0,
2 * 60 * 60,
0.025,
0,
);
await group.reloadAll(client);
const perpMarket = group.getPerpMarketByMarketIndex(

View File

@ -1,16 +1,9 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import copy from 'fast-copy';
import { cpuUsage } from 'process';
import { Group } from '../../src/accounts/group';
import { HealthCache } from '../../src/accounts/healthCache';
import { HealthType, MangoAccount } from '../../src/accounts/mangoAccount';
import { PerpMarket } from '../../src/accounts/perp';
import { Serum3Market } from '../../src/accounts/serum3';
import { MangoAccount } from '../../src/accounts/mangoAccount';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
import { ZERO_I80F48 } from '../../src/numbers/I80F48';
import { toUiDecimalsForQuote } from '../../src/utils';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
@ -27,56 +20,56 @@ async function debugUser(
group: Group,
mangoAccount: MangoAccount,
): Promise<void> {
console.log(mangoAccount.toString(group));
// console.log(mangoAccount.toString(group));
await mangoAccount.reload(client);
console.log(
'mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
);
console.log(
'mangoAccount.getHealth(HealthType.init) ' +
toUiDecimalsForQuote(
mangoAccount.getHealth(group, HealthType.init)!.toNumber(),
),
);
console.log(
'HealthCache.fromMangoAccount(group,mangoAccount).health(HealthType.init) ' +
toUiDecimalsForQuote(
HealthCache.fromMangoAccount(group, mangoAccount)
.health(HealthType.init)
.toNumber(),
),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.init) ' +
mangoAccount.getHealthRatio(group, HealthType.init)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.init) ' +
mangoAccount.getHealthRatioUi(group, HealthType.init),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.maint) ' +
mangoAccount.getHealthRatio(group, HealthType.maint)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
mangoAccount.getHealthRatioUi(group, HealthType.maint),
);
console.log(
'mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)!.toNumber()),
);
console.log(
'mangoAccount.getAssetsValue() ' +
toUiDecimalsForQuote(mangoAccount.getAssetsValue(group)!.toNumber()),
);
console.log(
'mangoAccount.getLiabsValue() ' +
toUiDecimalsForQuote(mangoAccount.getLiabsValue(group)!.toNumber()),
);
// console.log(
// 'mangoAccount.getEquity() ' +
// toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
// );
// console.log(
// 'mangoAccount.getHealth(HealthType.init) ' +
// toUiDecimalsForQuote(
// mangoAccount.getHealth(group, HealthType.init)!.toNumber(),
// ),
// );
// console.log(
// 'HealthCache.fromMangoAccount(group,mangoAccount).health(HealthType.init) ' +
// toUiDecimalsForQuote(
// HealthCache.fromMangoAccount(group, mangoAccount)
// .health(HealthType.init)
// .toNumber(),
// ),
// );
// console.log(
// 'mangoAccount.getHealthRatio(HealthType.init) ' +
// mangoAccount.getHealthRatio(group, HealthType.init)!.toNumber(),
// );
// console.log(
// 'mangoAccount.getHealthRatioUi(HealthType.init) ' +
// mangoAccount.getHealthRatioUi(group, HealthType.init),
// );
// console.log(
// 'mangoAccount.getHealthRatio(HealthType.maint) ' +
// mangoAccount.getHealthRatio(group, HealthType.maint)!.toNumber(),
// );
// console.log(
// 'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
// mangoAccount.getHealthRatioUi(group, HealthType.maint),
// );
// console.log(
// 'mangoAccount.getCollateralValue() ' +
// toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)!.toNumber()),
// );
// console.log(
// 'mangoAccount.getAssetsValue() ' +
// toUiDecimalsForQuote(mangoAccount.getAssetsValue(group)!.toNumber()),
// );
// console.log(
// 'mangoAccount.getLiabsValue() ' +
// toUiDecimalsForQuote(mangoAccount.getLiabsValue(group)!.toNumber()),
// );
async function getMaxWithdrawWithBorrowForTokenUiWrapper(
token,
@ -93,178 +86,178 @@ async function debugUser(
await getMaxWithdrawWithBorrowForTokenUiWrapper(srcToken);
}
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
// Turn on for debugging specific pairs
// if (src != 'USDC' || tgt != 'MNGO') return;
// function getMaxSourceForTokenSwapWrapper(src, tgt): void {
// // Turn on for debugging specific pairs
// // if (src != 'USDC' || tgt != 'MNGO') return;
let maxSourceUi;
try {
maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
);
} catch (error) {
console.log(`Error for ${src}->${tgt}, ` + error.toString());
}
// let maxSourceUi;
// try {
// maxSourceUi = mangoAccount.getMaxSourceUiForTokenSwap(
// group,
// group.banksMapByName.get(src)![0].mint,
// group.banksMapByName.get(tgt)![0].mint,
// );
// } catch (error) {
// console.log(`Error for ${src}->${tgt}, ` + error.toString());
// }
const maxTargetUi =
maxSourceUi *
(group.banksMapByName.get(src)![0].uiPrice /
group.banksMapByName.get(tgt)![0].uiPrice);
// const maxTargetUi =
// maxSourceUi *
// (group.banksMapByName.get(src)![0].uiPrice /
// group.banksMapByName.get(tgt)![0].uiPrice);
const sim = mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [
{
mintPk: group.banksMapByName.get(src)![0].mint,
uiTokenAmount: -maxSourceUi,
},
{
mintPk: group.banksMapByName.get(tgt)![0].mint,
uiTokenAmount: maxTargetUi,
},
]);
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
maxSourceUi.toFixed(3).padStart(10) +
`, health ratio after (${sim.toFixed(3).padStart(10)})`,
);
}
for (const srcToken of Array.from(group.banksMapByName.keys()).sort()) {
for (const tgtToken of Array.from(group.banksMapByName.keys()).sort()) {
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
}
}
// const sim = mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [
// {
// mintPk: group.banksMapByName.get(src)![0].mint,
// uiTokenAmount: -maxSourceUi,
// },
// {
// mintPk: group.banksMapByName.get(tgt)![0].mint,
// uiTokenAmount: maxTargetUi,
// },
// ]);
// console.log(
// `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
// maxSourceUi.toFixed(3).padStart(10) +
// `, health ratio after (${sim.toFixed(3).padStart(10)})`,
// );
// }
// for (const srcToken of Array.from(group.banksMapByName.keys()).sort()) {
// for (const tgtToken of Array.from(group.banksMapByName.keys()).sort()) {
// getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
// }
// }
function getMaxForPerpWrapper(perpMarket: PerpMarket): void {
const maxQuoteUi = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
);
const simMaxQuote = mangoAccount.simHealthRatioWithPerpBidUiChanges(
group,
perpMarket.perpMarketIndex,
maxQuoteUi / perpMarket.uiPrice,
);
const maxBaseUi = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
);
const simMaxBase = mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
maxBaseUi,
);
console.log(
`getMaxPerp ${perpMarket.name.padStart(
10,
)} getMaxQuoteForPerpBidUi ${maxQuoteUi
.toFixed(3)
.padStart(10)} health ratio after (${simMaxQuote
.toFixed(3)
.padStart(10)}), getMaxBaseForPerpAskUi ${maxBaseUi
.toFixed(3)
.padStart(10)} health ratio after (${simMaxBase
.toFixed(3)
.padStart(10)})`,
);
}
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
getMaxForPerpWrapper(perpMarket);
}
// function getMaxForPerpWrapper(perpMarket: PerpMarket): void {
// const maxQuoteUi = mangoAccount.getMaxQuoteForPerpBidUi(
// group,
// perpMarket.perpMarketIndex,
// );
// const simMaxQuote = mangoAccount.simHealthRatioWithPerpBidUiChanges(
// group,
// perpMarket.perpMarketIndex,
// maxQuoteUi / perpMarket.uiPrice,
// );
// const maxBaseUi = mangoAccount.getMaxBaseForPerpAskUi(
// group,
// perpMarket.perpMarketIndex,
// );
// const simMaxBase = mangoAccount.simHealthRatioWithPerpAskUiChanges(
// group,
// perpMarket.perpMarketIndex,
// maxBaseUi,
// );
// console.log(
// `getMaxPerp ${perpMarket.name.padStart(
// 10,
// )} getMaxQuoteForPerpBidUi ${maxQuoteUi
// .toFixed(3)
// .padStart(10)} health ratio after (${simMaxQuote
// .toFixed(3)
// .padStart(10)}), getMaxBaseForPerpAskUi ${maxBaseUi
// .toFixed(3)
// .padStart(10)} health ratio after (${simMaxBase
// .toFixed(3)
// .padStart(10)})`,
// );
// }
// for (const perpMarket of Array.from(
// group.perpMarketsMapByMarketIndex.values(),
// )) {
// getMaxForPerpWrapper(perpMarket);
// }
function getMaxForSerum3Wrapper(serum3Market: Serum3Market): void {
console.log(
`getMaxQuoteForSerum3BidUi ${serum3Market.name} ` +
mangoAccount.getMaxQuoteForSerum3BidUi(
group,
serum3Market.serumMarketExternal,
),
);
console.log(
`- simHealthRatioWithSerum3BidUiChanges ${serum3Market.name} ` +
mangoAccount.simHealthRatioWithSerum3BidUiChanges(
group,
mangoAccount.getMaxQuoteForSerum3BidUi(
group,
serum3Market.serumMarketExternal,
),
serum3Market.serumMarketExternal,
HealthType.init,
),
);
console.log(
`getMaxBaseForSerum3AskUi ${serum3Market.name} ` +
mangoAccount.getMaxBaseForSerum3AskUi(
group,
serum3Market.serumMarketExternal,
),
);
console.log(
`- simHealthRatioWithSerum3BidUiChanges ${serum3Market.name} ` +
mangoAccount.simHealthRatioWithSerum3AskUiChanges(
group,
mangoAccount.getMaxBaseForSerum3AskUi(
group,
serum3Market.serumMarketExternal,
),
serum3Market.serumMarketExternal,
HealthType.init,
),
);
}
for (const serum3Market of Array.from(
group.serum3MarketsMapByExternal.values(),
)) {
getMaxForSerum3Wrapper(serum3Market);
}
// function getMaxForSerum3Wrapper(serum3Market: Serum3Market): void {
// console.log(
// `getMaxQuoteForSerum3BidUi ${serum3Market.name} ` +
// mangoAccount.getMaxQuoteForSerum3BidUi(
// group,
// serum3Market.serumMarketExternal,
// ),
// );
// console.log(
// `- simHealthRatioWithSerum3BidUiChanges ${serum3Market.name} ` +
// mangoAccount.simHealthRatioWithSerum3BidUiChanges(
// group,
// mangoAccount.getMaxQuoteForSerum3BidUi(
// group,
// serum3Market.serumMarketExternal,
// ),
// serum3Market.serumMarketExternal,
// HealthType.init,
// ),
// );
// console.log(
// `getMaxBaseForSerum3AskUi ${serum3Market.name} ` +
// mangoAccount.getMaxBaseForSerum3AskUi(
// group,
// serum3Market.serumMarketExternal,
// ),
// );
// console.log(
// `- simHealthRatioWithSerum3BidUiChanges ${serum3Market.name} ` +
// mangoAccount.simHealthRatioWithSerum3AskUiChanges(
// group,
// mangoAccount.getMaxBaseForSerum3AskUi(
// group,
// serum3Market.serumMarketExternal,
// ),
// serum3Market.serumMarketExternal,
// HealthType.init,
// ),
// );
// }
// for (const serum3Market of Array.from(
// group.serum3MarketsMapByExternal.values(),
// )) {
// getMaxForSerum3Wrapper(serum3Market);
// }
// Liquidation price for perp positions
for (const pp of mangoAccount.perpActive()) {
const pm = group.getPerpMarketByMarketIndex(pp.marketIndex);
const health = toUiDecimalsForQuote(
mangoAccount.getHealth(group, HealthType.maint),
);
// // Liquidation price for perp positions
// for (const pp of mangoAccount.perpActive()) {
// const pm = group.getPerpMarketByMarketIndex(pp.marketIndex);
// const health = toUiDecimalsForQuote(
// mangoAccount.getHealth(group, HealthType.maint),
// );
if (
// pp.getNotionalValueUi(pm) > 1000 &&
// !(pp.getNotionalValueUi(pm) < health && pp.getBasePosition(pm).isPos())
// eslint-disable-next-line no-constant-condition
true
) {
const then = Date.now();
const startUsage = cpuUsage();
// if (
// // pp.getNotionalValueUi(pm) > 1000 &&
// // !(pp.getNotionalValueUi(pm) < health && pp.getBasePosition(pm).isPos())
// // eslint-disable-next-line no-constant-condition
// true
// ) {
// const then = Date.now();
// const startUsage = cpuUsage();
const lp = await pp.getLiquidationPrice(group, mangoAccount);
if (lp == null || lp.lt(ZERO_I80F48())) {
continue;
}
const lpUi = group
.getPerpMarketByMarketIndex(pp.marketIndex)
.priceNativeToUi(lp.toNumber());
// const lp = await pp.getLiquidationPrice(group, mangoAccount);
// if (lp == null || lp.lt(ZERO_I80F48())) {
// continue;
// }
// const lpUi = group
// .getPerpMarketByMarketIndex(pp.marketIndex)
// .priceNativeToUi(lp.toNumber());
const gClone: Group = copy(group);
gClone.getPerpMarketByMarketIndex(pm.perpMarketIndex)._price = lp;
// const gClone: Group = copy(group);
// gClone.getPerpMarketByMarketIndex(pm.perpMarketIndex)._price = lp;
const simHealth = toUiDecimalsForQuote(
mangoAccount.getHealth(gClone, HealthType.maint),
);
// const simHealth = toUiDecimalsForQuote(
// mangoAccount.getHealth(gClone, HealthType.maint),
// );
const now = Date.now();
const endUsage = cpuUsage(startUsage);
// const now = Date.now();
// const endUsage = cpuUsage(startUsage);
console.log(
` - ${pm.name}, health: ${health.toLocaleString()}, side: ${
pp.getBasePosition(pm).isPos() ? 'LONG' : 'SHORT'
}, notional: ${pp
.getNotionalValueUi(pm)
.toLocaleString()}, liq price: ${lpUi.toLocaleString()}, sim health: ${simHealth.toLocaleString()}, time ${
now - then
}ms, cpu usage ${(endUsage['user'] / 1000).toLocaleString()}ms`,
);
}
}
// console.log(
// ` - ${pm.name}, health: ${health.toLocaleString()}, side: ${
// pp.getBasePosition(pm).isPos() ? 'LONG' : 'SHORT'
// }, notional: ${pp
// .getNotionalValueUi(pm)
// .toLocaleString()}, liq price: ${lpUi.toLocaleString()}, sim health: ${simHealth.toLocaleString()}, time ${
// now - then
// }ms, cpu usage ${(endUsage['user'] / 1000).toLocaleString()}ms`,
// );
// }
// }
}
async function main(): Promise<void> {

View File

@ -94,6 +94,7 @@ async function main() {
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
PublicKey.default,
0, // tokenIndex
'USDC',
{
@ -124,6 +125,7 @@ async function main() {
group,
solDevnetMint,
solDevnetOracle,
PublicKey.default,
1, // tokenIndex
'SOL',
{
@ -218,6 +220,7 @@ async function main() {
1.0,
2 * 60 * 60,
0.025,
0,
);
await group.reloadAll(client);
const perpMarket = group.getPerpMarketByMarketIndex(0 as PerpMarketIndex);

View File

@ -206,6 +206,7 @@ async function registerTokens() {
group,
usdcMainnetMint,
usdcMainnetOracle.publicKey,
PublicKey.default,
0,
'USDC',
{
@ -226,6 +227,7 @@ async function registerTokens() {
group,
usdtMainnetMint,
usdtMainnetOracle,
PublicKey.default,
1,
'USDT',
{
@ -246,6 +248,7 @@ async function registerTokens() {
group,
daiMainnetMint,
daiMainnetOracle,
PublicKey.default,
2,
'DAI',
{
@ -266,6 +269,7 @@ async function registerTokens() {
group,
ethMainnetMint,
ethMainnetOracle,
PublicKey.default,
3,
'ETH',
{
@ -286,6 +290,7 @@ async function registerTokens() {
group,
solMainnetMint,
solMainnetOracle,
PublicKey.default,
4,
'SOL',
{
@ -306,6 +311,7 @@ async function registerTokens() {
group,
msolMainnetMint,
msolMainnetOracle,
PublicKey.default,
5,
'MSOL',
{
@ -450,6 +456,7 @@ async function registerPerpMarkets() {
1.0,
2 * 60 * 60,
0.025,
0,
);
await client.perpCreateMarket(
@ -482,6 +489,7 @@ async function registerPerpMarkets() {
1.0,
2 * 60 * 60,
0.2, // 20% positive pnl liquidation fee?
0,
);
}

View File

@ -60,31 +60,31 @@ async function buildClient(): Promise<MangoClient> {
);
}
async function groupEdit(): Promise<void> {
const client = await buildClient();
const group = await client.getGroup(new PublicKey(GROUP_PK));
const ix = await client.program.methods
.groupEdit(
null, // admin
null, // fastListingAdmin
null, // securityAdmin
null, // testing
null, // version
null, // depositLimitQuote
null, // feesPayWithMngo
null, // feesMngoBonusRate
null, // feesSwapMangoAccount
6, // feesMngoTokenIndex
null, // feesExpiryInterval
5, // allowedFastListingsPerInterval
)
.accounts({
group: group.publicKey,
admin: group.admin,
})
.instruction();
console.log(serializeInstructionToBase64(ix));
}
// async function groupEdit(): Promise<void> {
// const client = await buildClient();
// const group = await client.getGroup(new PublicKey(GROUP_PK));
// const ix = await client.program.methods
// .groupEdit(
// null, // admin
// null, // fastListingAdmin
// null, // securityAdmin
// null, // testing
// null, // version
// null, // depositLimitQuote
// null, // feesPayWithMngo
// null, // feesMngoBonusRate
// null, // feesSwapMangoAccount
// 6, // feesMngoTokenIndex
// null, // feesExpiryInterval
// 5, // allowedFastListingsPerInterval
// )
// .accounts({
// group: group.publicKey,
// admin: group.admin,
// })
// .instruction();
// console.log(serializeInstructionToBase64(ix));
// }
// async function tokenRegister(): Promise<void> {
// const client = await buildClient();
@ -265,6 +265,7 @@ async function perpCreate(): Promise<void> {
1,
new BN(60 * 60),
percentageToDecimal(10),
0,
)
.accounts({
group: group.publicKey,
@ -358,6 +359,7 @@ async function perpEdit(): Promise<void> {
params.positivePnlLiquidationFee,
params.name,
params.forceClose,
params.platformLiquidationFee,
)
.accounts({
group: group.publicKey,
@ -466,7 +468,7 @@ async function idlSetAuthority(): Promise<void> {
async function main(): Promise<void> {
try {
await groupEdit();
// await groupEdit();
// await tokenRegister();
// await tokenEdit();
// await perpCreate();

View File

@ -18,9 +18,12 @@ import {
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import chunk from 'lodash/chunk';
import { chunk } from 'lodash';
import { updateVoterWeightRecord } from './updateVoteWeightRecord';
import { VsrClient } from './voteStakeRegistryClient';
import { createComputeBudgetIx } from '../../src/utils/rpc';
import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions';
import { SequenceType } from '@blockworks-foundation/mangolana/lib/globalTypes';
export const MANGO_MINT = 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac';
export const MANGO_REALM_PK = new PublicKey(
@ -98,23 +101,27 @@ export const createProposal = async (
const insertInstructions: TransactionInstruction[] = [];
for (const i in proposalInstructions) {
const instruction = getInstructionDataFromBase64(
serializeInstructionToBase64(proposalInstructions[i]),
);
await withInsertTransaction(
insertInstructions,
MANGO_GOVERNANCE_PROGRAM,
programVersion,
governance,
proposalAddress,
tokenOwnerRecord.pubkey,
governanceAuthority,
Number(i),
0,
0,
[instruction],
payer,
);
try {
const instruction = getInstructionDataFromBase64(
serializeInstructionToBase64(proposalInstructions[i]),
);
await withInsertTransaction(
insertInstructions,
MANGO_GOVERNANCE_PROGRAM,
programVersion,
governance,
proposalAddress,
tokenOwnerRecord.pubkey,
governanceAuthority,
Number(i),
0,
0,
[instruction],
payer,
);
} catch (e) {
console.log(e, '@@@@@@@');
}
}
if (signOff) {
const signatoryRecordAddress = await getSignatoryRecordAddress(
@ -137,28 +144,29 @@ export const createProposal = async (
const txChunks = chunk([...instructions, ...insertInstructions], 2);
const transactions: Transaction[] = [];
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
for (const chunk of txChunks) {
const tx = new Transaction();
tx.add(...chunk);
tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
tx.recentBlockhash = latestBlockhash.blockhash;
tx.feePayer = payer;
transactions.push(tx);
}
await sendSignAndConfirmTransactions({
connection,
wallet,
transactionInstructions: txChunks.map((txChunk) => ({
instructionsSet: [
{
signers: [],
transactionInstruction: createComputeBudgetIx(80000),
},
...txChunk.map((tx) => ({
signers: [],
transactionInstruction: tx,
})),
],
sequenceType: SequenceType.Sequential,
})),
config: {
maxRetries: 5,
autoRetry: true,
maxTxesInBatch: 20,
logFlowInfo: true,
},
});
const signedTransactions = await wallet.signAllTransactions(transactions);
for (const tx of signedTransactions) {
const rawTransaction = tx.serialize();
const address = await connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
});
await connection.confirmTransaction({
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: address,
});
}
return proposalAddress;
};

View File

@ -202,7 +202,7 @@ async function main(): Promise<void> {
const genericBanks = ['MNGO', 'MSOL'];
let nextTokenIndex = 3;
for (let name of genericBanks) {
for (const name of genericBanks) {
console.log(`Registering ${name}...`);
const mint = new PublicKey(MAINNET_MINTS.get(name)!);
const oracle = oracles.get(name);
@ -239,7 +239,7 @@ async function main(): Promise<void> {
}
let nextSerumMarketIndex = 0;
for (let [name, mint] of MAINNET_MINTS) {
for (const [name, mint] of MAINNET_MINTS) {
if (name == 'USDC') {
continue;
}

View File

@ -1,6 +1,6 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as splToken from '@solana/spl-token';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Bank } from '../../src/accounts/bank';
import {
@ -280,6 +280,7 @@ async function main() {
group,
newMint,
newOracle.publicKey,
PublicKey.default,
newTokenIndex,
'TMP',
{

View File

@ -143,6 +143,7 @@ async function registerTokens(): Promise<void> {
group,
usdcMainnetMint,
usdcMainnetOracle,
PublicKey.default,
0,
'USDC',
defaultTokenParams,

View File

@ -0,0 +1,140 @@
// import { createComputeBudgetIx } from '@blockworks-foundation/mango-v4';
// import { PublicKey, chunk } from '@metaplex-foundation/js';
// import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes';
// import { Connection, Keypair, Transaction } from '@solana/web3.js';
// import {
// SwitchboardProgram,
// QueueAccount,
// CrankAccount,
// AggregatorAccount,
// } from '@switchboard-xyz/solana.js';
// import { MANGO_DAO_WALLET } from './governance/constants';
// import { OracleJob } from '@switchboard-xyz/common';
// import { Wallet } from '@coral-xyz/anchor';
// import { awaitTransactionSignatureConfirmation } from '@blockworks-foundation/mangolana/lib/transactions';
// const newOracleName = 'NOS/USD';
// const oldOraclePk = '2FGoL9PNhNGpduRKLsTa4teRaX3vfarXAc1an2KyXxQm';
// const SWITCHBOARD_PERMISSIONLESS_QUE =
// '5JYwqvKkqp35w8Nq3ba4z1WYUeJQ1rB36V8XvaGp6zn1';
// const SWITCHBOARD_PERMISSIONLESS_CRANK =
// 'BKtF8yyQsj3Ft6jb2nkfpEKzARZVdGgdEPs6mFmZNmbA';
// async function run() {
// const WALLET = new Wallet(Keypair.fromSecretKey(bs58.decode('')));
// const connection = new Connection('https://api.mngo.cloud/lite-rpc/v1/');
// const program = await SwitchboardProgram.load(connection);
// const payer = WALLET.publicKey;
// const [[queueAccount], [crankAccount]] = await Promise.all([
// QueueAccount.load(program, SWITCHBOARD_PERMISSIONLESS_QUE),
// CrankAccount.load(program, SWITCHBOARD_PERMISSIONLESS_CRANK),
// ]);
// const [aggregatorAccountOld, aggregatorAccountDataOld] =
// await AggregatorAccount.load(program, new PublicKey(oldOraclePk));
// const jobs = await aggregatorAccountOld.loadJobs(aggregatorAccountDataOld);
// const newJobs: string[] = [];
// for (const job of jobs) {
// const jobYaml = job.job.toYaml();
// const remove_after = jobYaml.indexOf('- multiplyTask');
// let result = jobYaml.substring(0, remove_after);
// result =
// result +
// `- conditionalTask:
// attempt:
// - multiplyTask:
// job:
// tasks:
// - oracleTask:
// pythAddress: Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD
// pythAllowedConfidenceInterval: 10
// onFailure:
// - multiplyTask:
// job:
// tasks:
// - oracleTask:
// switchboardAddress: FwYfsmj5x8YZXtQBNo2Cz8TE7WRCMFqA6UTffK4xQKMH`;
// newJobs.push(result);
// }
// const [aggregatorAccountNew, txArray1] =
// await queueAccount.createFeedInstructions(payer, {
// name: newOracleName,
// batchSize: aggregatorAccountDataOld.oracleRequestBatchSize,
// minRequiredOracleResults: aggregatorAccountDataOld.minOracleResults,
// minRequiredJobResults: aggregatorAccountDataOld.minJobResults,
// minUpdateDelaySeconds: aggregatorAccountDataOld.minUpdateDelaySeconds,
// forceReportPeriod: 60 * 60,
// withdrawAuthority: MANGO_DAO_WALLET,
// authority: payer,
// crankDataBuffer: crankAccount.dataBuffer?.publicKey,
// crankPubkey: crankAccount.publicKey,
// fundAmount: 0.1,
// slidingWindow: true,
// disableCrank: false,
// maxPriorityFeeMultiplier: 5,
// priorityFeeBumpPeriod: 10,
// priorityFeeBump: 1000,
// basePriorityFee: 1000,
// jobs: [
// ...newJobs.map((x) => ({
// weight: 1,
// data: OracleJob.encodeDelimited(OracleJob.fromYaml(x)).finish(),
// })),
// ],
// });
// const lockTx = aggregatorAccountNew.lockInstruction(payer, {});
// const transferAuthIx = aggregatorAccountNew.setAuthorityInstruction(payer, {
// newAuthority: MANGO_DAO_WALLET,
// });
// const latestBlockhash = await connection.getLatestBlockhash('processed');
// const txChunks = chunk([...txArray1, lockTx, transferAuthIx], 1);
// const transactions: Transaction[] = [];
// for (const chunkIndex in txChunks) {
// const chunk = txChunks[chunkIndex];
// const tx = new Transaction();
// const singers = [...chunk.flatMap((x) => x.signers)];
// tx.add(createComputeBudgetIx(800000));
// tx.add(...chunk.flatMap((x) => x.ixns));
// tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
// tx.recentBlockhash = latestBlockhash.blockhash;
// tx.feePayer = payer;
// if (singers.length) {
// tx.sign(...singers);
// }
// transactions.push(tx);
// }
// const signedTxes = await WALLET.signAllTransactions(transactions);
// for (const signed of signedTxes) {
// const rawTransaction = signed.serialize();
// const signature = await connection.sendRawTransaction(rawTransaction, {
// skipPreflight: true,
// });
// await awaitTransactionSignatureConfirmation({
// txid: signature,
// confirmLevel: 'processed',
// connection: connection,
// timeoutStrategy: {
// block: latestBlockhash,
// },
// config: {
// logFlowInfo: true,
// },
// });
// }
// console.log(aggregatorAccountNew.publicKey.toBase58(), '@@@@@');
// }
// try {
// run();
// } catch (e) {
// console.log(e);
// }

View File

@ -27,7 +27,6 @@ import { MangoAccount } from '../src/accounts/mangoAccount';
import { MangoClient } from '../src/client';
import { NullTokenEditParams } from '../src/clientIxParamBuilder';
import { MANGO_V4_MAIN_GROUP as MANGO_V4_PRIMARY_GROUP } from '../src/constants';
import { I80F48 } from '../src/numbers/I80F48';
import {
LiqorPriceImpact,
buildGroupGrid,
@ -190,8 +189,7 @@ async function updateTokenParams(): Promise<void> {
const builder = Builder(NullTokenEditParams);
let change = false;
// eslint-disable-next-line no-constant-condition
if (true) {
try {
const tier = Object.values(LISTING_PRESETS).find((x) =>
x.initLiabWeight.toFixed(1) === '1.8'
? x.initLiabWeight.toFixed(1) ===
@ -200,27 +198,75 @@ async function updateTokenParams(): Promise<void> {
: x.initLiabWeight.toFixed(1) ===
bank?.initLiabWeight.toNumber().toFixed(1),
);
if (!tier) {
console.log(`${bank.name}, no tier found`);
} else {
console.log(
`${bank.name.padStart(10)}, ${bank.loanFeeRate
.mul(I80F48.fromNumber(100))
.toFixed(2)}, ${bank.loanOriginationFeeRate
.mul(I80F48.fromNumber(100))
.toFixed(2)}, ${tier?.preset_name.padStart(5)}, ${(
tier.loanFeeRate * 100
).toFixed(2)}, ${(tier!.loanOriginationFeeRate * 100).toFixed(2)}`,
);
builder.loanFeeRate(tier!.loanFeeRate);
builder.loanOriginationFeeRate(tier!.loanOriginationFeeRate);
builder.flashLoanSwapFeeRate(tier!.loanOriginationFeeRate);
change = true;
// eslint-disable-next-line no-constant-condition
if (true) {
if (
bank.uiBorrows() == 0 &&
bank.reduceOnly == 2 &&
bank.initAssetWeight.toNumber() == 0 &&
bank.maintAssetWeight.toNumber() == 0
) {
builder.disableAssetLiquidation(true);
builder.oracleConfig({
confFilter: 1000,
maxStalenessSlots: -1,
});
change = true;
}
}
}
try {
// // eslint-disable-next-line no-constant-condition
// if (true) {
// if (bank.uiBorrows() == 0 && bank.reduceOnly == 1) {
// builder.disableAssetLiquidation(true);
// builder.forceWithdraw(true);
// change = true;
// }
// }
// // eslint-disable-next-line no-constant-condition
// if (true) {
// if (!tier) {
// console.log(`${bank.name}, no tier found`);
// } else if (tier.preset_name != 'C') {
// if (tier.preset_name.includes('A')) {
// builder.liquidationFee(bank.liquidationFee.toNumber() * 0.2);
// builder.platformLiquidationFee(
// bank.liquidationFee.toNumber() * 0.8,
// );
// } else if (tier.preset_name.includes('B')) {
// builder.liquidationFee(bank.liquidationFee.toNumber() * 0.4);
// builder.platformLiquidationFee(
// bank.liquidationFee.toNumber() * 0.6,
// );
// }
// change = true;
// }
// }
// eslint-disable-next-line no-constant-condition
// if (true) {
// if (!tier) {
// console.log(`${bank.name}, no tier found`);
// } else {
// console.log(
// `${bank.name.padStart(10)}, ${bank.loanFeeRate
// .mul(I80F48.fromNumber(100))
// .toFixed(2)}, ${bank.loanOriginationFeeRate
// .mul(I80F48.fromNumber(100))
// .toFixed(2)}, ${tier?.preset_name.padStart(5)}, ${(
// tier.loanFeeRate * 100
// ).toFixed(2)}, ${(tier!.loanOriginationFeeRate * 100).toFixed(2)}`,
// );
// builder.loanFeeRate(tier!.loanFeeRate);
// builder.loanOriginationFeeRate(tier!.loanOriginationFeeRate);
// builder.flashLoanSwapFeeRate(tier!.loanOriginationFeeRate);
// change = true;
// }
// }
// formulas are sourced from here
// https://www.notion.so/mango-markets/Mango-v4-Risk-parameter-recommendations-d309cdf5faac4aeea7560356e68532ab
@ -230,175 +276,176 @@ async function updateTokenParams(): Promise<void> {
// 4 * priceImpact.target_amount,
// );
if (!bank.areBorrowsReduceOnly()) {
// eslint-disable-next-line no-constant-condition
if (false) {
// Net borrow limits
const netBorrowLimitPerWindowQuote = Math.max(
10_000,
Math.min(bank.uiDeposits() * bank.uiPrice, 300_000) / 3 +
Math.max(0, bank.uiDeposits() * bank.uiPrice - 300_000) / 5,
// eslint-disable-next-line no-constant-condition
if (false) {
// Net borrow limits
const netBorrowLimitPerWindowQuote = Math.max(
10_000,
Math.min(bank.uiDeposits() * bank.uiPrice, 300_000) / 3 +
Math.max(0, bank.uiDeposits() * bank.uiPrice - 300_000) / 5,
);
builder.netBorrowLimitPerWindowQuote(
toNativeI80F48ForQuote(netBorrowLimitPerWindowQuote).toNumber(),
);
change = true;
if (
netBorrowLimitPerWindowQuote !=
toUiDecimalsForQuote(bank.netBorrowLimitPerWindowQuote)
) {
console.log(
`${
bank.name
} new - ${netBorrowLimitPerWindowQuote.toLocaleString()}, old - ${toUiDecimalsForQuote(
bank.netBorrowLimitPerWindowQuote,
).toLocaleString()}`,
);
builder.netBorrowLimitPerWindowQuote(
toNativeI80F48ForQuote(netBorrowLimitPerWindowQuote).toNumber(),
);
change = true;
if (
netBorrowLimitPerWindowQuote !=
toUiDecimalsForQuote(bank.netBorrowLimitPerWindowQuote)
) {
console.log(
`${
bank.name
} new - ${netBorrowLimitPerWindowQuote.toLocaleString()}, old - ${toUiDecimalsForQuote(
bank.netBorrowLimitPerWindowQuote,
).toLocaleString()}`,
}
}
// Deposit limits
// eslint-disable-next-line no-constant-condition
if (false) {
if (bank.maintAssetWeight.toNumber() > 0) {
{
// Find asset's largest batch in $ we would need to liquidate, batches are extreme points of a range of price drop,
// range is constrained by leverage provided
// i.e. how much volatility we expect
const r = findLargestAssetBatchUi(
pisForLiqor,
bank.name,
Math.round(bank.maintAssetWeight.toNumber() * 100),
100 - Math.round(bank.maintAssetWeight.toNumber() * 100),
stepSize,
);
const maxLiqBatchQuoteUi = r[0];
const maxLiqBatchUi = r[1];
const sellImpact = getPriceImpactForBank(
midPriceImpacts,
bank,
(bank.liquidationFee.toNumber() * 100) / 2,
);
// Deposit limit = sell impact - largest batch
const allowedNewDepositsQuoteUi =
sellImpact.target_amount - maxLiqBatchQuoteUi;
const allowedNewDepositsUi =
sellImpact.target_amount / bank.uiPrice -
maxLiqBatchQuoteUi / bank.uiPrice;
const depositLimitUi = bank.uiDeposits() + allowedNewDepositsUi;
// LOG
// console.log(
// `${bank.name.padStart(20)} ${maxLiqBatchUi
// .toLocaleString()
// .padStart(15)} ${maxLiqBatchQuoteUi
// .toLocaleString()
// .padStart(15)}$ ${sellImpact.target_amount
// .toLocaleString()
// .padStart(12)}$ ${sellImpact.avg_price_impact_percent
// .toLocaleString()
// .padStart(12)}% ${allowedNewDepositsUi
// .toLocaleString()
// .padStart(20)}${allowedNewDepositsQuoteUi
// .toLocaleString()
// .padStart(20)}$ ${bank
// .uiDeposits()
// .toLocaleString()
// .padStart(12)} ${(bank.uiDeposits() * bank.uiPrice)
// .toLocaleString()
// .padStart(12)}$ ${depositLimitUi
// .toLocaleString()
// .padStart(12)}`,
// );
builder.depositLimit(toNative(depositLimitUi, bank.mintDecimals));
change = true;
}
}
}
// Deposit limits
// eslint-disable-next-line no-constant-condition
if (false) {
if (bank.maintAssetWeight.toNumber() > 0) {
{
// Find asset's largest batch in $ we would need to liquidate, batches are extreme points of a range of price drop,
// range is constrained by leverage provided
// i.e. how much volatility we expect
const r = findLargestAssetBatchUi(
pisForLiqor,
bank.name,
Math.round(bank.maintAssetWeight.toNumber() * 100),
100 - Math.round(bank.maintAssetWeight.toNumber() * 100),
stepSize,
);
const params = builder.build();
console.log(
`${bank.name}, ${params.disableAssetLiquidation} ${params.oracleConfig?.maxStalenessSlots} ${params.oracleConfig?.confFilter}`,
);
const maxLiqBatchQuoteUi = r[0];
const maxLiqBatchUi = r[1];
const ix = await client.program.methods
.tokenEdit(
params.oracle,
params.oracleConfig,
params.groupInsuranceFund,
params.interestRateParams,
params.loanFeeRate,
params.loanOriginationFeeRate,
params.maintAssetWeight,
params.initAssetWeight,
params.maintLiabWeight,
params.initLiabWeight,
params.liquidationFee,
params.stablePriceDelayIntervalSeconds,
params.stablePriceDelayGrowthLimit,
params.stablePriceGrowthLimit,
params.minVaultToDepositsRatio,
params.netBorrowLimitPerWindowQuote !== null
? new BN(params.netBorrowLimitPerWindowQuote)
: null,
params.netBorrowLimitWindowSizeTs !== null
? new BN(params.netBorrowLimitWindowSizeTs)
: null,
params.borrowWeightScaleStartQuote,
params.depositWeightScaleStartQuote,
params.resetStablePrice ?? false,
params.resetNetBorrowLimit ?? false,
params.reduceOnly,
params.name,
params.forceClose,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.interestCurveScaling,
params.interestTargetUtilization,
params.maintWeightShiftStart,
params.maintWeightShiftEnd,
params.maintWeightShiftAssetTarget,
params.maintWeightShiftLiabTarget,
params.maintWeightShiftAbort ?? false,
false, // setFallbackOracle, unused
params.depositLimit,
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
params.forceWithdraw,
)
.accounts({
group: group.publicKey,
oracle: bank.oracle,
admin: group.admin,
mintInfo: group.mintInfosMapByTokenIndex.get(bank.tokenIndex)
?.publicKey,
fallbackOracle: PublicKey.default,
})
.remainingAccounts([
{
pubkey: bank.publicKey,
isWritable: true,
isSigner: false,
} as AccountMeta,
])
.instruction();
const sellImpact = getPriceImpactForBank(
midPriceImpacts,
bank,
(bank.liquidationFee.toNumber() * 100) / 2,
);
const tx = new Transaction({ feePayer: wallet.publicKey }).add(ix);
const simulated = await client.connection.simulateTransaction(tx);
// Deposit limit = sell impact - largest batch
const allowedNewDepositsQuoteUi =
sellImpact.target_amount - maxLiqBatchQuoteUi;
const allowedNewDepositsUi =
sellImpact.target_amount / bank.uiPrice -
maxLiqBatchQuoteUi / bank.uiPrice;
if (simulated.value.err) {
console.log('error', simulated.value.logs);
throw simulated.value.logs;
}
const depositLimitUi = bank.uiDeposits() + allowedNewDepositsUi;
// LOG
// console.log(
// `${bank.name.padStart(20)} ${maxLiqBatchUi
// .toLocaleString()
// .padStart(15)} ${maxLiqBatchQuoteUi
// .toLocaleString()
// .padStart(15)}$ ${sellImpact.target_amount
// .toLocaleString()
// .padStart(12)}$ ${sellImpact.avg_price_impact_percent
// .toLocaleString()
// .padStart(12)}% ${allowedNewDepositsUi
// .toLocaleString()
// .padStart(20)}${allowedNewDepositsQuoteUi
// .toLocaleString()
// .padStart(20)}$ ${bank
// .uiDeposits()
// .toLocaleString()
// .padStart(12)} ${(bank.uiDeposits() * bank.uiPrice)
// .toLocaleString()
// .padStart(12)}$ ${depositLimitUi
// .toLocaleString()
// .padStart(12)}`,
// );
builder.depositLimit(
toNative(depositLimitUi, bank.mintDecimals),
);
change = true;
}
}
}
const params = builder.build();
console.log(bank.name);
console.log(params.loanFeeRate);
console.log(params.loanOriginationFeeRate);
console.log(params.flashLoanSwapFeeRate);
const ix = await client.program.methods
.tokenEdit(
params.oracle,
params.oracleConfig,
params.groupInsuranceFund,
params.interestRateParams,
params.loanFeeRate,
params.loanOriginationFeeRate,
params.maintAssetWeight,
params.initAssetWeight,
params.maintLiabWeight,
params.initLiabWeight,
params.liquidationFee,
params.stablePriceDelayIntervalSeconds,
params.stablePriceDelayGrowthLimit,
params.stablePriceGrowthLimit,
params.minVaultToDepositsRatio,
params.netBorrowLimitPerWindowQuote !== null
? new BN(params.netBorrowLimitPerWindowQuote)
: null,
params.netBorrowLimitWindowSizeTs !== null
? new BN(params.netBorrowLimitWindowSizeTs)
: null,
params.borrowWeightScaleStartQuote,
params.depositWeightScaleStartQuote,
params.resetStablePrice ?? false,
params.resetNetBorrowLimit ?? false,
params.reduceOnly,
params.name,
params.forceClose,
params.tokenConditionalSwapTakerFeeRate,
params.tokenConditionalSwapMakerFeeRate,
params.flashLoanSwapFeeRate,
params.interestCurveScaling,
params.interestTargetUtilization,
params.maintWeightShiftStart,
params.maintWeightShiftEnd,
params.maintWeightShiftAssetTarget,
params.maintWeightShiftLiabTarget,
params.maintWeightShiftAbort ?? false,
false, // setFallbackOracle, unused
params.depositLimit,
)
.accounts({
group: group.publicKey,
oracle: bank.oracle,
admin: group.admin,
mintInfo: group.mintInfosMapByTokenIndex.get(bank.tokenIndex)
?.publicKey,
})
.remainingAccounts([
{
pubkey: bank.publicKey,
isWritable: true,
isSigner: false,
} as AccountMeta,
])
.instruction();
const tx = new Transaction({ feePayer: wallet.publicKey }).add(ix);
const simulated = await client.connection.simulateTransaction(tx);
if (simulated.value.err) {
console.log('error', simulated.value.logs);
throw simulated.value.logs;
}
if (change) {
instructions.push(ix);
}
if (change) {
instructions.push(ix);
}
} catch (error) {
console.log(`....Skipping ${bank.name}, ${error}`);
@ -433,7 +480,7 @@ async function updateTokenParams(): Promise<void> {
tokenOwnerRecord,
PROPOSAL_TITLE
? PROPOSAL_TITLE
: 'Update loan fee, loan origination fee, and flash loan fees in mango-v4',
: 'Disable asset liquidation for C tier tokens in mango-v4, part 2',
PROPOSAL_LINK ?? '',
Object.values(proposals).length,
instructions,

View File

@ -1,7 +1,6 @@
import { BN } from '@coral-xyz/anchor';
import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { format } from 'path';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { As, toUiDecimals } from '../utils';
import { OracleProvider, isOracleStaleOrUnconfident } from './oracle';
@ -686,6 +685,7 @@ export class MintInfo {
vaults: PublicKey[];
oracle: PublicKey;
registrationTime: BN;
fallbackOracle: PublicKey;
groupInsuranceFund: number;
},
): MintInfo {
@ -698,6 +698,7 @@ export class MintInfo {
obj.vaults,
obj.oracle,
obj.registrationTime,
obj.fallbackOracle,
obj.groupInsuranceFund == 1,
);
}
@ -711,6 +712,7 @@ export class MintInfo {
public vaults: PublicKey[],
public oracle: PublicKey,
public registrationTime: BN,
public fallbackOracle: PublicKey,
public groupInsuranceFund: boolean,
) {}

View File

@ -811,7 +811,7 @@ export class HealthCache {
}
}
private static binaryApproximationSearch(
static binaryApproximationSearch(
left: I80F48,
leftValue: I80F48,
right: I80F48,
@ -830,10 +830,8 @@ export class HealthCache {
// );
if (
(leftValue.sub(targetValue).isPos() &&
rightValue.sub(targetValue).isPos()) ||
(leftValue.sub(targetValue).isNeg() &&
rightValue.sub(targetValue).isNeg())
(leftValue.gt(targetValue) && rightValue.gt(targetValue)) ||
(leftValue.lt(targetValue) && rightValue.lt(targetValue))
) {
throw new Error(
`Internal error: left ${leftValue.toNumber()} and right ${rightValue.toNumber()} don't contain the target value ${targetValue.toNumber()}!`,

View File

@ -1,10 +1,18 @@
import { PublicKey } from '@solana/web3.js';
import { MangoAccount } from './mangoAccount';
import {
HealthType,
MangoAccount,
TokenPosition,
TokenPositionDto,
} from './mangoAccount';
import BN from 'bn.js';
import { Bank } from './bank';
import { toNative, toUiDecimals } from '../utils';
import { Bank, TokenIndex } from './bank';
import { deepClone, toNative, toUiDecimals } from '../utils';
import { expect } from 'chai';
import { I80F48 } from '../numbers/I80F48';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { Group } from './group';
import { HealthCache } from './healthCache';
import { assert } from 'console';
describe('Mango Account', () => {
const mangoAccount = new MangoAccount(
@ -79,3 +87,238 @@ describe('Mango Account', () => {
done();
});
});
describe('maxWithdraw', () => {
const protoAccount = new MangoAccount(
PublicKey.default,
PublicKey.default,
PublicKey.default,
[],
PublicKey.default,
0,
false,
false,
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
0,
0,
[],
[],
[],
[],
[],
new Map(),
);
protoAccount.tokens.push(
new TokenPosition(ZERO_I80F48(), 0 as TokenIndex, 0, ZERO_I80F48(), 0, 0),
);
protoAccount.tokens.push(
new TokenPosition(ZERO_I80F48(), 1 as TokenIndex, 0, ZERO_I80F48(), 0, 0),
);
const protoBank = {
vault: PublicKey.default,
mint: PublicKey.default,
tokenIndex: 0,
price: ONE_I80F48(),
getAssetPrice() {
return this.price;
},
getLiabPrice() {
return this.price;
},
stablePriceModel: { stablePrice: ONE_I80F48() },
initAssetWeight: I80F48.fromNumber(0.8),
initLiabWeight: I80F48.fromNumber(1.2),
maintWeights() {
return [I80F48.fromNumber(0.9), I80F48.fromNumber(1.1)];
},
scaledInitAssetWeight(price) {
return this.initAssetWeight;
},
scaledInitLiabWeight(price) {
return this.initLiabWeight;
},
loanOriginationFeeRate: I80F48.fromNumber(0.001),
minVaultToDepositsRatio: I80F48.fromNumber(0.1),
depositIndex: I80F48.fromNumber(1000000),
borrowIndex: I80F48.fromNumber(1000000),
indexedDeposits: I80F48.fromNumber(0),
indexedBorrows: I80F48.fromNumber(0),
nativeDeposits() {
return this.depositIndex.mul(this.indexedDeposits);
},
nativeBorrows() {
return this.borrowIndex.mul(this.indexedBorrows);
},
areBorrowsReduceOnly() {
return false;
},
} as any as Bank;
function makeGroup(bank0, bank1, vaultAmount) {
return {
getFirstBankByMint(mint) {
if (mint.equals(bank0.mint)) {
return bank0;
} else if (mint.equals(bank1.mint)) {
return bank1;
}
},
getFirstBankByTokenIndex(tokenIndex) {
return [bank0, bank1][tokenIndex];
},
getFirstBankForPerpSettlement() {
return bank0;
},
vaultAmountsMap: new Map<string, BN>([
[bank0.vault.toBase58(), new BN(vaultAmount)],
]),
} as any as Group;
}
function setup(vaultAmount): [Group, Bank, Bank, MangoAccount] {
const account = deepClone<MangoAccount>(protoAccount);
const bank0 = deepClone(protoBank);
const bank1 = deepClone(protoBank);
bank1.tokenIndex = 1 as TokenIndex;
bank1.mint = PublicKey.unique();
bank1.initAssetWeight = ONE_I80F48();
bank1.initLiabWeight = ONE_I80F48();
const group = makeGroup(bank0, bank1, vaultAmount);
return [group, bank0, bank1, account];
}
function deposit(bank, account, amount) {
const amountV = I80F48.fromNumber(amount);
const indexedAmount = amountV.div(bank.depositIndex);
if (indexedAmount.mul(bank.depositIndex).lt(amountV)) {
const delta = new I80F48(new BN(1));
indexedAmount.iadd(delta);
}
bank.indexedDeposits.iadd(indexedAmount);
const tp = account.tokens[bank.tokenIndex];
assert(!tp.indexedPosition.isNeg());
tp.indexedPosition.iadd(indexedAmount);
}
function borrow(bank, account, amount) {
const indexedAmount = I80F48.fromNumber(amount).div(bank.borrowIndex);
bank.indexedBorrows.iadd(indexedAmount);
const tp = account.tokens[bank.tokenIndex];
assert(!tp.indexedPosition.isPos());
tp.indexedPosition.isub(indexedAmount);
}
function maxWithdraw(group, account) {
return account
.getMaxWithdrawWithBorrowForToken(group, PublicKey.default)
.toNumber();
}
it('full withdraw', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
deposit(bank0, account, 100);
expect(maxWithdraw(group, account)).equal(100);
done();
});
it('full withdraw limited vault', (done) => {
const [group, bank0, bank1, account] = setup(90);
deposit(bank0, account, 100);
expect(maxWithdraw(group, account)).equal(90);
done();
});
it('full withdraw limited utilization', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
const other = deepClone(account);
deposit(bank0, account, 100);
borrow(bank0, other, 50);
expect(maxWithdraw(group, account)).equal(50);
done();
});
it('withdraw limited health', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
deposit(bank0, account, 100);
borrow(bank1, account, 50);
expect(maxWithdraw(group, account)).equal(Math.floor(100 - 50 / 0.8));
done();
});
it('pure borrow', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
const other = deepClone(account);
deposit(bank0, other, 1000); // so there's something to borrow
deposit(bank1, account, 100);
expect(maxWithdraw(group, account)).equal(Math.floor(100 / 1.2));
done();
});
it('pure borrow limited utilization', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
const other = deepClone(account);
deposit(bank0, other, 50);
deposit(bank1, account, 100);
expect(maxWithdraw(group, account)).equal(44); // due to origination fees!
bank0.loanOriginationFeeRate = ZERO_I80F48();
expect(maxWithdraw(group, account)).equal(45);
done();
});
it('withdraw and borrow', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
const other = deepClone(account);
deposit(bank0, account, 100);
deposit(bank1, account, 100);
deposit(bank0, other, 10000);
expect(maxWithdraw(group, account)).equal(100 + Math.floor(100 / 1.2));
done();
});
it('withdraw limited health and scaling', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
bank0.scaledInitAssetWeight = function (price) {
const startScale = I80F48.fromNumber(50);
if (this.nativeDeposits().gt(startScale)) {
return this.initAssetWeight.div(this.nativeDeposits().div(startScale));
}
return this.initAssetWeight;
};
const other = deepClone(account);
deposit(bank0, other, 100);
deposit(bank0, account, 200);
borrow(bank1, account, 20);
// initial account health = 200 * 0.8 * 50 / 300 - 20 = 6.66
// zero account health = 100 * 0.8 * 50 / 200 - 20 = 0
// so can withdraw 100
expect(maxWithdraw(group, account)).equal(100);
done();
});
it('borrow limited health and scaling', (done) => {
const [group, bank0, bank1, account] = setup(1000000);
bank0.scaledInitLiabWeight = function (price) {
const startScale = I80F48.fromNumber(50);
if (this.nativeBorrows().gt(startScale)) {
return this.initLiabWeight.mul(this.nativeBorrows().div(startScale));
}
return this.initLiabWeight;
};
const other = deepClone(account);
deposit(bank0, other, 100);
deposit(bank1, account, 100);
// -64*1.2*64/50+100 = 1.69
// -65*1.2*65/50+100 = -1.4
expect(maxWithdraw(group, account)).equal(64);
done();
});
});

View File

@ -4,9 +4,16 @@ import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
import { AccountInfo, PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { OPENBOOK_PROGRAM_ID, RUST_I64_MAX, RUST_I64_MIN } from '../constants';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import {
I80F48,
I80F48Dto,
MAX_I80F48,
ONE_I80F48,
ZERO_I80F48,
} from '../numbers/I80F48';
import {
U64_MAX_BN,
deepClone,
roundTo5,
toNativeI80F48,
toUiDecimals,
@ -536,67 +543,126 @@ export class MangoAccount {
mintPk: PublicKey,
): I80F48 {
const tokenBank: Bank = group.getFirstBankByMint(mintPk);
const initHealth = this.getHealth(group, HealthType.init);
const loanOriginationFactor = ONE_I80F48().add(
tokenBank.loanOriginationFeeRate,
);
const maxBorrowUtilization = I80F48.fromNumber(
1 - tokenBank.minVaultToDepositsRatio,
);
const tp = this.getToken(tokenBank.tokenIndex);
const healthCache = HealthCache.fromMangoAccount(group, this);
const tokenInfoIndex = healthCache.getOrCreateTokenInfoIndex(tokenBank);
const initHealth = healthCache.health(HealthType.init);
// Case 1:
// Cannot withdraw if init health is below 0
if (initHealth.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
// Deposits need special treatment since they would neither count towards liabilities
// nor would be charged loanOriginationFeeRate when withdrawn
// Step 1: Since withdraws can change the asset weight scaling and borrows will
// change the liab weight scaling, we use a binary search to find something
// close to the true maximum value.
// To do that, we first get an upper bound that the search can start with.
const tp = this.getToken(tokenBank.tokenIndex);
const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48();
let existingPositionHealthContrib = ZERO_I80F48();
if (existingTokenDeposits.gt(ZERO_I80F48())) {
existingPositionHealthContrib = existingTokenDeposits
.mul(tokenBank.getAssetPrice())
.imul(tokenBank.scaledInitAssetWeight(tokenBank.getAssetPrice()));
}
// Case 2: token deposits have higher contribution than initHealth,
// can withdraw without borrowing until initHealth reaches 0
if (existingPositionHealthContrib.gt(initHealth)) {
const withdrawAbleExistingPositionHealthContrib = initHealth;
return withdrawAbleExistingPositionHealthContrib
.div(tokenBank.scaledInitAssetWeight(tokenBank.getAssetPrice()))
.div(tokenBank.getAssetPrice());
}
// Case 3: withdraw = withdraw existing deposits + borrows until initHealth reaches 0
const initHealthWithoutExistingPosition = initHealth.sub(
existingPositionHealthContrib,
const lowerBoundBorrowHealthFactor = tokenBank
.getLiabPrice()
.mul(tokenBank.scaledInitLiabWeight(tokenBank.getLiabPrice()));
const upperBound = existingTokenDeposits.add(
initHealth.div(lowerBoundBorrowHealthFactor),
);
let maxBorrowNative = initHealthWithoutExistingPosition
.div(tokenBank.scaledInitLiabWeight(tokenBank.price))
.div(tokenBank.price);
// Cap maxBorrow to maintain minVaultToDepositsRatio on the bank
// Step 2: Find the maximum withdraw amount
const mutTokenBank = deepClone<Bank>(tokenBank);
const mutHealthCache = deepClone<HealthCache>(healthCache);
const invalidHealthValue = MAX_I80F48().div(I80F48.fromNumber(2)).neg();
function healthAfterWithdraw(amount: I80F48): I80F48 {
const withdrawOfDepositsAmount = amount.min(existingTokenDeposits);
const borrowAmount = amount.sub(withdrawOfDepositsAmount);
// Take care of loan origination fee
const borrowCost = borrowAmount.mul(loanOriginationFactor);
// Update the account's token position
const mutTi = mutHealthCache.tokenInfos[tokenInfoIndex];
const startTi = healthCache.tokenInfos[tokenInfoIndex];
mutTi.balanceSpot = startTi.balanceSpot
.sub(withdrawOfDepositsAmount)
.sub(borrowCost);
// Update the bank and the scaled weights
mutTokenBank.indexedDeposits = tokenBank.indexedDeposits.sub(
withdrawOfDepositsAmount.div(tokenBank.depositIndex),
);
mutTokenBank.indexedBorrows = tokenBank.indexedBorrows.add(
borrowCost.div(tokenBank.borrowIndex),
);
if (mutTokenBank.nativeBorrows().gt(mutTokenBank.nativeDeposits())) {
return invalidHealthValue;
}
if (borrowAmount.isPos()) {
if (
mutTokenBank
.nativeBorrows()
.gt(mutTokenBank.nativeDeposits().mul(maxBorrowUtilization))
) {
return invalidHealthValue;
}
}
mutTi.initScaledAssetWeight = mutTokenBank.scaledInitAssetWeight(
tokenBank.getAssetPrice(),
);
mutTi.initScaledLiabWeight = mutTokenBank.scaledInitLiabWeight(
tokenBank.getLiabPrice(),
);
return mutHealthCache.health(HealthType.init);
}
// Withdrawing one token will change health by at least this much.
// We use this to define a good stopping criterion for the search.
const minHealthChangePerNative = tokenBank
.getAssetPrice()
.mul(tokenBank.scaledInitAssetWeight(tokenBank.getAssetPrice()));
let amount = HealthCache.binaryApproximationSearch(
ZERO_I80F48(),
initHealth,
upperBound,
I80F48.fromNumber(0.5).mul(minHealthChangePerNative).min(initHealth),
I80F48.fromNumber(0.5),
healthAfterWithdraw,
{
maxIterations: 100,
targetError: I80F48.fromNumber(0.2)
.mul(minHealthChangePerNative)
.toNumber(),
},
);
// Step 3: Only full tokens can be withdrawn, do the rounding and
// check if withdrawing one-native more would also be fine
amount = amount.floor();
const amountPlusOne = amount.add(ONE_I80F48());
if (!healthAfterWithdraw(amountPlusOne).isNeg()) {
amount = amountPlusOne;
}
// Step 4: No borrows on no-borrow tokens
if (tokenBank.areBorrowsReduceOnly()) {
amount = amount.min(existingTokenDeposits);
}
// Step 5: also limit by vault funds
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 vaultLimit = I80F48.fromU64(vaultAmount);
const maxBorrowNativeWithoutFees = maxBorrowNative.div(
ONE_I80F48().add(tokenBank.loanOriginationFeeRate),
);
return maxBorrowNativeWithoutFees.add(existingTokenDeposits);
return amount.min(vaultLimit).max(ZERO_I80F48());
}
public getMaxWithdrawWithBorrowForTokenUi(

View File

@ -229,9 +229,26 @@ export class MangoClient {
ixs: TransactionInstruction[],
opts: SendTransactionOpts = {},
): Promise<MangoSignatureStatus> {
const alts =
opts?.alts && opts?.alts?.length
? opts.alts
: group.addressLookupTablesList;
const uniqueAccountsCount = [
...new Set([
...ixs.flatMap((x) => x.keys.map((x) => x.pubkey.toBase58())),
...ixs.flatMap((x) => x.programId.toBase58()),
...alts.map((x) => x.key.toBase58()),
]),
].length;
if (uniqueAccountsCount > 64) {
throw new Error(`Max accounts limit exceeded`);
}
return await this.sendAndConfirmTransaction(ixs, {
alts: group.addressLookupTablesList,
...opts,
alts: alts,
});
}