Health: Add soft deposit and borrow limits

This commit is contained in:
Christian Kamm 2022-11-29 09:47:03 +01:00
parent 512eef96ea
commit cf34a5b4b7
15 changed files with 325 additions and 54 deletions

View File

@ -81,6 +81,39 @@ pub fn division_i80f48_f64(a: I80F48, b: I80F48) -> I80F48 {
r
}
#[inline(never)]
pub fn mul_f64(a: f64, b: f64) -> f64 {
msg!("mul_f64");
sol_log_compute_units();
let r = a * b;
if r.is_nan() {
panic!("nan"); // here as a side-effect to avoid reordering
}
sol_log_compute_units();
r
}
#[inline(never)]
pub fn mul_i80f48(a: I80F48, b: I80F48) -> I80F48 {
msg!("mul_i80f48");
sol_log_compute_units();
let r = a.checked_mul(b).unwrap();
sol_log_compute_units();
r
}
#[inline(never)]
pub fn i80f48_to_f64(a: I80F48) -> f64 {
msg!("i80f48_to_f64");
sol_log_compute_units();
let r = a.to_num::<f64>();
if r.is_nan() {
panic!("nan"); // here as a side-effect to avoid reordering
}
sol_log_compute_units();
r
}
pub fn benchmark(_ctx: Context<Benchmark>) -> Result<()> {
// 101000
// 477
@ -98,6 +131,8 @@ pub fn benchmark(_ctx: Context<Benchmark>) -> Result<()> {
division_i128(a.to_bits(), b.to_bits()); // 100 - 2000 CU
division_i80f48_30bit(a, b); // 300 CU
division_i80f48_f64(a, b); // 500 CU
mul_i80f48(a >> 64, b >> 64); // 100 CU
i80f48_to_f64(a); // 50 CU
}
{
@ -126,6 +161,12 @@ pub fn benchmark(_ctx: Context<Benchmark>) -> Result<()> {
division_u32(a, b); // 20 CU
}
{
let a = clock.slot as f64;
let b = clock.unix_timestamp as f64;
mul_f64(a, b); // 0 CU??
}
sol_log_compute_units(); // 100321 -> 101
msg!("msg!"); // 100079+101 -> 203
sol_log_compute_units(); // 100117

View File

@ -218,7 +218,7 @@ pub fn serum3_liq_force_cancel_orders(
after_base_vault,
before_base_vault,
)?
.adjust_health_cache(&mut health_cache)?;
.adjust_health_cache(&mut health_cache, &base_bank)?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
@ -227,7 +227,7 @@ pub fn serum3_liq_force_cancel_orders(
after_quote_vault,
before_quote_vault,
)?
.adjust_health_cache(&mut health_cache)?;
.adjust_health_cache(&mut health_cache, &quote_bank)?;
//
// Health check at the end

View File

@ -340,8 +340,8 @@ pub fn serum3_place_order(
require_gte!(before_vault, after_vault);
// Charge the difference in vault balance to the user's account
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
let vault_difference = {
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
@ -356,7 +356,7 @@ pub fn serum3_place_order(
// Health check
//
if let Some((mut health_cache, pre_health)) = pre_health_opt {
vault_difference.adjust_health_cache(&mut health_cache)?;
vault_difference.adjust_health_cache(&mut health_cache, &payer_bank)?;
oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?;
account.check_health_post(&health_cache, pre_health)?;
}
@ -408,8 +408,9 @@ pub struct VaultDifference {
}
impl VaultDifference {
pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> {
health_cache.adjust_token_balance(self.token_index, self.native_change)?;
pub fn adjust_health_cache(&self, health_cache: &mut HealthCache, bank: &Bank) -> Result<()> {
assert_eq!(bank.token_index, self.token_index);
health_cache.adjust_token_balance(bank, self.native_change)?;
Ok(())
}
}

View File

@ -297,7 +297,7 @@ pub fn token_liq_bankruptcy(
let liab_bank = bank_ais[0].load::<Bank>()?;
let end_liab_native = liqee_liab.native(&liab_bank);
liqee_health_cache
.adjust_token_balance(liab_token_index, cm!(end_liab_native - initial_liab_native))?;
.adjust_token_balance(&liab_bank, cm!(end_liab_native - initial_liab_native))?;
// Check liqee health again
let liqee_init_health = liqee_health_cache.health(HealthType::Init);

View File

@ -187,12 +187,10 @@ pub fn token_liq_with_token(
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
// Update the health cache
liqee_health_cache
.adjust_token_balance(&liab_bank, cm!(liqee_liab_native_after - liqee_liab_native))?;
liqee_health_cache.adjust_token_balance(
liab_token_index,
cm!(liqee_liab_native_after - liqee_liab_native),
)?;
liqee_health_cache.adjust_token_balance(
asset_token_index,
&asset_bank,
cm!(liqee_assets_native_after - liqee_asset_native),
)?;

View File

@ -151,7 +151,9 @@ pub fn token_register(
* net_borrows_window_size_ts,
net_borrows_limit_native,
net_borrows_window_native: 0,
reserved: [0; 2136],
borrow_limit_quote: f64::MAX,
collateral_limit_quote: f64::MAX,
reserved: [0; 2120],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);

View File

@ -126,7 +126,9 @@ pub fn token_register_trustless(
* net_borrows_window_size_ts,
net_borrows_limit_native: 1_000_000,
net_borrows_window_native: 0,
reserved: [0; 2136],
borrow_limit_quote: f64::MAX,
collateral_limit_quote: f64::MAX,
reserved: [0; 2120],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);

View File

@ -146,8 +146,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// Health check
//
if let Some((mut health_cache, pre_health)) = pre_health_opt {
health_cache
.adjust_token_balance(token_index, cm!(native_position_after - native_position))?;
health_cache.adjust_token_balance(&bank, cm!(native_position_after - native_position))?;
account.check_health_post(&health_cache, pre_health)?;
}

View File

@ -113,8 +113,25 @@ pub struct Bank {
pub net_borrows_limit_native: i64,
pub net_borrows_window_native: i64,
/// Soft borrow limit in native quote
///
/// Once the borrows on the bank exceed this quote value, init_liab_weight is scaled up.
/// Set to f64::MAX to disable.
///
/// See scaled_init_liab_weight().
pub borrow_limit_quote: f64,
/// Limit for collateral of deposits
///
/// Once the deposits in the bank exceed this quote value, init_asset_weight is scaled
/// down to keep the total collateral value constant.
/// Set to f64::MAX to disable.
///
/// See scaled_init_asset_weight().
pub collateral_limit_quote: f64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 2136],
pub reserved: [u8; 2120],
}
const_assert_eq!(size_of::<Bank>(), 3112);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -175,7 +192,9 @@ impl Bank {
net_borrows_window_size_ts: existing_bank.net_borrows_window_size_ts,
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
net_borrows_window_native: 0,
reserved: [0; 2136],
borrow_limit_quote: f64::MAX,
collateral_limit_quote: f64::MAX,
reserved: [0; 2120],
}
}
@ -185,12 +204,14 @@ impl Bank {
.trim_matches(char::from(0))
}
#[inline(always)]
pub fn native_borrows(&self) -> I80F48 {
self.borrow_index * self.indexed_borrows
cm!(self.borrow_index * self.indexed_borrows)
}
#[inline(always)]
pub fn native_deposits(&self) -> I80F48 {
self.deposit_index * self.indexed_deposits
cm!(self.deposit_index * self.indexed_deposits)
}
/// Deposits `native_amount`.
@ -692,6 +713,47 @@ impl Bank {
pub fn stable_price(&self) -> I80F48 {
I80F48::from_num(self.stable_price_model.stable_price)
}
/// Returns the init asset weight, adjusted for the number of deposits on the bank.
///
/// If max_collateral is 0, then the scaled init weight will be 0.
/// Otherwise the weight is unadjusted until max_collateral and then scaled down
/// such that scaled_init_weight * deposits remains constant.
#[inline(always)]
pub fn scaled_init_asset_weight(&self, price: I80F48) -> I80F48 {
if self.collateral_limit_quote == f64::MAX {
return self.init_asset_weight;
}
// The next line is around 500 CU
let deposits_quote = self.native_deposits().to_num::<f64>() * price.to_num::<f64>();
if deposits_quote <= self.collateral_limit_quote {
self.init_asset_weight
} else {
// The next line is around 500 CU
let scale = self.collateral_limit_quote / deposits_quote;
cm!(self.init_asset_weight * I80F48::from_num(scale))
}
}
#[inline(always)]
pub fn scaled_init_liab_weight(&self, price: I80F48) -> I80F48 {
if self.borrow_limit_quote == f64::MAX {
return self.init_liab_weight;
}
// The next line is around 500 CU
let borrows_quote = self.native_borrows().to_num::<f64>() * price.to_num::<f64>();
if borrows_quote <= self.borrow_limit_quote {
self.init_liab_weight
} else if self.borrow_limit_quote == 0.0 {
// TODO: will certainly cause overflow, so it's not exactly what is needed; health should be -MAX?
// maybe handling this case isn't super helpful?
I80F48::MAX
} else {
// The next line is around 500 CU
let scale = borrows_quote / self.borrow_limit_quote;
cm!(self.init_liab_weight * I80F48::from_num(scale))
}
}
}
#[macro_export]

View File

@ -5,7 +5,7 @@ use fixed_macro::types::I80F48;
use crate::error::*;
use crate::state::{
MangoAccountFixed, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition,
Bank, MangoAccountFixed, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition,
Serum3MarketIndex, TokenIndex,
};
use crate::util::checked_math as cm;
@ -365,10 +365,15 @@ impl HealthCache {
.ok_or_else(|| error_msg!("token index {} not found", token_index))
}
pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> {
let entry_index = self.token_info_index(token_index)?;
/// Changes the cached user account token balance.
pub fn adjust_token_balance(&mut self, bank: &Bank, change: I80F48) -> Result<()> {
let entry_index = self.token_info_index(bank.token_index)?;
let mut entry = &mut self.token_infos[entry_index];
entry.init_asset_weight =
bank.scaled_init_asset_weight(entry.prices.asset(HealthType::Init));
entry.init_liab_weight = bank.scaled_init_liab_weight(entry.prices.liab(HealthType::Init));
// Work around the fact that -((-x) * y) == x * y does not hold for I80F48:
// We need to make sure that if balance is before * price, then change = -before
// brings it to exactly zero.
@ -377,6 +382,20 @@ impl HealthCache {
Ok(())
}
/// Recomputes the dynamic init weights for the bank's current deposits/borrows.
pub fn recompute_token_weights(&mut self, bank: &Bank) -> Result<()> {
let entry_index = self.token_info_index(bank.token_index)?;
let mut entry = &mut self.token_infos[entry_index];
entry.init_asset_weight =
bank.scaled_init_asset_weight(entry.prices.asset(HealthType::Init));
entry.init_liab_weight = bank.scaled_init_liab_weight(entry.prices.liab(HealthType::Init));
Ok(())
}
/// Changes the cached user account token and serum balances.
///
/// WARNING: You must also call recompute_token_weights() after all bank
/// deposit/withdraw changes!
pub fn adjust_serum3_reserved(
&mut self,
market_index: Serum3MarketIndex,
@ -604,16 +623,17 @@ pub fn new_health_cache(
retriever.bank_and_oracle(&account.fixed.group, i, position.token_index)?;
let native = position.native(bank);
let prices = Prices {
oracle: oracle_price,
stable: bank.stable_price(),
};
token_infos.push(TokenInfo {
token_index: bank.token_index,
maint_asset_weight: bank.maint_asset_weight,
init_asset_weight: bank.init_asset_weight,
init_asset_weight: bank.scaled_init_asset_weight(prices.asset(HealthType::Init)),
maint_liab_weight: bank.maint_liab_weight,
init_liab_weight: bank.init_liab_weight,
prices: Prices {
oracle: oracle_price,
stable: bank.stable_price(),
},
init_liab_weight: bank.scaled_init_liab_weight(prices.liab(HealthType::Init)),
prices,
balance_native: native,
});
}
@ -787,6 +807,14 @@ mod tests {
));
}
#[derive(Default)]
struct BankSettings {
deposits: u64,
borrows: u64,
collateral_limit_quote: u64,
borrow_limit_quote: u64,
}
#[derive(Default)]
struct TestHealth1Case {
token1: i64,
@ -796,6 +824,7 @@ mod tests {
oo_1_3: (u64, u64),
perp1: (i64, i64, i64, i64),
expected_health: f64,
bank_settings: [BankSettings; 3],
}
fn test_health1_runner(testcase: &TestHealth1Case) {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
@ -830,6 +859,21 @@ mod tests {
DUMMY_NOW_TS,
)
.unwrap();
for (settings, bank) in testcase
.bank_settings
.iter()
.zip([&mut bank1, &mut bank2, &mut bank3].iter_mut())
{
let bank = bank.data();
bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index;
bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index;
if settings.collateral_limit_quote > 0 {
bank.collateral_limit_quote = settings.collateral_limit_quote as f64;
}
if settings.borrow_limit_quote > 0 {
bank.borrow_limit_quote = settings.borrow_limit_quote as f64;
}
}
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account1 = account.create_serum3_orders(2).unwrap();
@ -995,6 +1039,66 @@ mod tests {
+ 20.0 * 0.8,
..Default::default()
},
TestHealth1Case { // 10, checking collateral limit
token1: 100,
token2: 100,
token3: 100,
bank_settings: [
BankSettings {
deposits: 100,
collateral_limit_quote: 1000,
..BankSettings::default()
},
BankSettings {
deposits: 1500,
collateral_limit_quote: 1000 * 5,
..BankSettings::default()
},
BankSettings {
deposits: 10000,
collateral_limit_quote: 1000 * 10,
..BankSettings::default()
},
],
expected_health:
// token1
0.8 * 100.0
// token2
+ 0.5 * 100.0 * 5.0 * (5000.0 / (1500.0 * 5.0))
// token3
+ 0.5 * 100.0 * 10.0 * (10000.0 / (10000.0 * 10.0)),
..Default::default()
},
TestHealth1Case { // 11, checking borrow limit
token1: -100,
token2: -100,
token3: -100,
bank_settings: [
BankSettings {
borrows: 100,
borrow_limit_quote: 1000,
..BankSettings::default()
},
BankSettings {
borrows: 1500,
borrow_limit_quote: 1000 * 5,
..BankSettings::default()
},
BankSettings {
borrows: 10000,
borrow_limit_quote: 1000 * 10,
..BankSettings::default()
},
],
expected_health:
// token1
-1.2 * 100.0
// token2
- 1.5 * 100.0 * 5.0 * (1500.0 * 5.0 / 5000.0)
// token3
- 1.5 * 100.0 * 10.0 * (10000.0 * 10.0 / 10000.0),
..Default::default()
},
];
for (i, testcase) in testcases.iter().enumerate() {

View File

@ -6,7 +6,7 @@ use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use crate::error::*;
use crate::state::{PerpMarketIndex, TokenIndex};
use crate::state::{Bank, MangoAccountValue, PerpMarketIndex};
use crate::util::checked_math as cm;
use super::*;
@ -43,6 +43,44 @@ impl HealthCache {
}
}
fn cache_after_swap(
&self,
account: &MangoAccountValue,
source_bank: &Bank,
target_bank: &Bank,
amount: I80F48,
price: I80F48,
) -> Self {
let mut source_position = account
.token_position(source_bank.token_index)
.map(|v| v.clone())
.unwrap_or_default();
let mut target_position = account
.token_position(target_bank.token_index)
.map(|v| v.clone())
.unwrap_or_default();
let target_amount = cm!(amount * price);
let mut source_bank = source_bank.clone();
source_bank
.withdraw_with_fee(&mut source_position, amount, 0)
.unwrap();
let mut target_bank = target_bank.clone();
target_bank
.deposit(&mut target_position, target_amount, 0)
.unwrap();
let mut resulting_cache = self.clone();
resulting_cache
.adjust_token_balance(&source_bank, -amount)
.unwrap();
resulting_cache
.adjust_token_balance(&target_bank, target_amount)
.unwrap();
resulting_cache
}
/// How much source native tokens may be swapped for target tokens while staying
/// above the min_ratio health ratio.
///
@ -54,8 +92,9 @@ impl HealthCache {
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
pub fn max_swap_source_for_health_ratio(
&self,
source: TokenIndex,
target: TokenIndex,
account: &MangoAccountValue,
source_bank: &Bank,
target_bank: &Bank,
price: I80F48,
min_ratio: I80F48,
) -> Result<I80F48> {
@ -74,8 +113,8 @@ impl HealthCache {
return Ok(I80F48::ZERO);
}
let source_index = find_token_info_index(&self.token_infos, source)?;
let target_index = find_token_info_index(&self.token_infos, target)?;
let source_index = find_token_info_index(&self.token_infos, source_bank.token_index)?;
let target_index = find_token_info_index(&self.token_infos, target_bank.token_index)?;
let source = &self.token_infos[source_index];
let target = &self.token_infos[target_index];
@ -89,10 +128,7 @@ impl HealthCache {
}
let cache_after_swap = |amount: I80F48| {
let mut adjusted_cache = self.clone();
adjusted_cache.token_infos[source_index].balance_native -= amount;
adjusted_cache.token_infos[target_index].balance_native += cm!(amount * price);
adjusted_cache
self.cache_after_swap(account, source_bank, target_bank, amount, price)
};
let health_ratio_after_swap =
|amount| cache_after_swap(amount).health_ratio(HealthType::Init);
@ -379,6 +415,15 @@ mod tests {
balance_native: I80F48::ZERO,
};
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let account = MangoAccountValue::from_bytes(&buffer).unwrap();
let group = Pubkey::new_unique();
let (mut bank0, _) = mock_bank_and_oracle(group, 0, 1.0, 0.1, 0.1);
let (mut bank1, _) = mock_bank_and_oracle(group, 1, 5.0, 0.2, 0.2);
let (mut bank2, _) = mock_bank_and_oracle(group, 2, 5.0, 0.3, 0.3);
let banks = [bank0.data(), bank1.data(), bank2.data()];
let health_cache = HealthCache {
token_infos: vec![
TokenInfo {
@ -407,8 +452,9 @@ mod tests {
assert_eq!(
health_cache
.max_swap_source_for_health_ratio(
0,
1,
&account,
banks[0],
banks[1],
I80F48::from_num(2.0 / 3.0),
I80F48::from_num(50.0)
)
@ -425,15 +471,17 @@ mod tests {
target: TokenIndex,
ratio: f64,
price_factor: f64| {
let mut c = c.clone();
let source_price = &c.token_infos[source as usize].prices;
let source_bank = &banks[source as usize];
let target_price = &c.token_infos[target as usize].prices;
let target_bank = &banks[target as usize];
let swap_price =
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
let source_amount = c
.max_swap_source_for_health_ratio(
source,
target,
&account,
source_bank,
target_bank,
swap_price,
I80F48::from_num(ratio),
)
@ -441,12 +489,16 @@ mod tests {
if source_amount == I80F48::MAX {
return (f64::MAX, f64::MAX);
}
c.adjust_token_balance(source, -source_amount).unwrap();
c.adjust_token_balance(target, source_amount * swap_price)
.unwrap();
let after_swap = c.cache_after_swap(
&account,
source_bank,
target_bank,
source_amount,
swap_price,
);
(
source_amount.to_num::<f64>(),
c.health_ratio(HealthType::Init).to_num::<f64>(),
after_swap.health_ratio(HealthType::Init).to_num::<f64>(),
)
};
let check_max_swap_result = |c: &HealthCache,

View File

@ -95,6 +95,8 @@ pub fn mock_bank_and_oracle(
bank.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights);
bank.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights);
bank.data().stable_price_model.reset_to_price(price, 0);
bank.data().collateral_limit_quote = f64::MAX;
bank.data().borrow_limit_quote = f64::MAX;
bank.data().net_borrows_window_size_ts = 1; // dummy
bank.data().net_borrows_limit_native = i64::MAX; // max since we don't want this to interfere
(bank, oracle)

View File

@ -13,7 +13,9 @@ mod program_test;
#[tokio::test]
async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
let context = TestContext::new().await;
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(85_000); // TokenLiqWithToken needs 84k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
@ -275,7 +277,9 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
#[tokio::test]
async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
let context = TestContext::new().await;
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(85_000); // TokenLiqWithToken needs 84k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();

View File

@ -39,7 +39,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await;
// TODO: actual explicit CU comparisons.
// On 2022-11-18 the final deposit costs 45495 CU and each new token increases it by roughly 1729 CU
// On 2022-11-29 the final deposit costs 61568 CU and each new token increases it by roughly 3125 CU
Ok(())
}
@ -47,7 +47,9 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
// Try to reach compute limits in health checks by having many serum markets in an account
#[tokio::test]
async fn test_health_compute_serum() -> Result<(), TransportError> {
let context = TestContext::new().await;
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(80_000);
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
@ -156,7 +158,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
}
// TODO: actual explicit CU comparisons.
// On 2022-11-18 the final deposit costs 62920 CU and each new market increases it by roughly 4820 CU
// On 2022-11-29 the final deposit costs 76029 CU and each new market increases it by roughly 6191 CU
Ok(())
}
@ -272,7 +274,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
}
// TODO: actual explicit CU comparisons.
// On 2022-11-18 the final deposit costs 50502 CU and each new market increases it by roughly 3037 CU
// On 2022-11-29 the final deposit costs 54954 CU and each new market increases it by roughly 3171 CU
Ok(())
}

View File

@ -173,7 +173,9 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
#[tokio::test]
async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
let context = TestContext::new().await;
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(85_000); // LiqTokenWithToken needs 79k
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();