mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
245 lines
7.8 KiB
Rust
245 lines
7.8 KiB
Rust
use amm_client::{
|
|
caller_amounts_to_stored, paired_amount_from_token_a, paired_amount_from_token_b,
|
|
pool_spot_change_bps, prepare_caller_opening_pair, prepare_minimum_opening_pair,
|
|
prepare_opening_from_token_a, prepare_opening_from_token_b, stored_amounts_to_caller,
|
|
validate_explicit_opening_pair, IntentError, OpeningLiquidityIntent, Q64_64_ONE,
|
|
};
|
|
use amm_core::{PoolDefinition, MINIMUM_LIQUIDITY};
|
|
use amm_program::quote::{self as program_quote, PairOrder, SwapDirection};
|
|
use nssa_core::account::AccountId;
|
|
|
|
const FEE_BPS: u128 = 30;
|
|
|
|
fn pool(reserve_a: u128, reserve_b: u128) -> PoolDefinition {
|
|
PoolDefinition {
|
|
definition_token_a_id: AccountId::new([1; 32]),
|
|
definition_token_b_id: AccountId::new([2; 32]),
|
|
vault_a_id: AccountId::new([3; 32]),
|
|
vault_b_id: AccountId::new([4; 32]),
|
|
liquidity_pool_id: AccountId::new([5; 32]),
|
|
liquidity_pool_supply: MINIMUM_LIQUIDITY
|
|
.checked_mul(100)
|
|
.expect("test liquidity supply fits u128"),
|
|
reserve_a,
|
|
reserve_b,
|
|
fees: FEE_BPS,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn minimum_pair_handles_prices_below_equal_and_above_one() {
|
|
for price in [Q64_64_ONE - 1, Q64_64_ONE, Q64_64_ONE + 1] {
|
|
let prepared = prepare_minimum_opening_pair(price, FEE_BPS).unwrap();
|
|
assert!(prepared.quote.user_liquidity > 0);
|
|
assert!(prepared.token_a_amount > 0);
|
|
assert!(prepared.token_b_amount > 0);
|
|
|
|
if price >= Q64_64_ONE && prepared.token_a_amount > 1 {
|
|
let previous_a = prepared.token_a_amount.checked_sub(1).unwrap();
|
|
let previous_b = paired_amount_from_token_a(previous_a, price).unwrap();
|
|
assert!(program_quote::create_pool(previous_a, previous_b, FEE_BPS).is_err());
|
|
} else if price < Q64_64_ONE && prepared.token_b_amount > 1 {
|
|
let previous_b = prepared.token_b_amount.checked_sub(1).unwrap();
|
|
let previous_a = paired_amount_from_token_b(previous_b, price).unwrap();
|
|
assert!(program_quote::create_pool(previous_a, previous_b, FEE_BPS).is_err());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_price_and_zero_edited_amount_are_rejected() {
|
|
assert_eq!(
|
|
prepare_minimum_opening_pair(0, FEE_BPS),
|
|
Err(IntentError::ZeroDesiredPrice)
|
|
);
|
|
assert_eq!(
|
|
paired_amount_from_token_a(0, Q64_64_ONE),
|
|
Err(IntentError::ZeroEditedAmount)
|
|
);
|
|
assert_eq!(
|
|
paired_amount_from_token_b(1, 0),
|
|
Err(IntentError::ZeroDesiredPrice)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pairing_uses_checked_widened_math_and_reports_overflow() {
|
|
assert_eq!(
|
|
paired_amount_from_token_a(u128::MAX, u128::MAX),
|
|
Err(IntentError::ArithmeticOverflow {
|
|
operation: "token-A to token-B pairing"
|
|
})
|
|
);
|
|
assert_eq!(
|
|
paired_amount_from_token_b(u128::MAX, 1),
|
|
Err(IntentError::ArithmeticOverflow {
|
|
operation: "token-B to token-A pairing"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn paired_and_explicit_amounts_are_validated_by_program_quote() {
|
|
let from_a = prepare_opening_from_token_a(2_000, Q64_64_ONE * 2, FEE_BPS).unwrap();
|
|
assert_eq!(from_a.token_b_amount, 4_000);
|
|
assert_eq!(from_a.quote.pool.reserve_b, 4_000);
|
|
|
|
let from_b = prepare_opening_from_token_b(4_000, Q64_64_ONE * 2, FEE_BPS).unwrap();
|
|
assert_eq!(from_b.token_a_amount, 2_000);
|
|
|
|
let explicit = validate_explicit_opening_pair(2_000, 4_000, Q64_64_ONE * 2, FEE_BPS).unwrap();
|
|
assert_eq!(explicit.actual_price_q64_64, Q64_64_ONE * 2);
|
|
|
|
let mismatch =
|
|
validate_explicit_opening_pair(2_000, 4_001, Q64_64_ONE * 2, FEE_BPS).unwrap_err();
|
|
assert!(matches!(mismatch, IntentError::SpotPriceMismatch { .. }));
|
|
|
|
let too_small = prepare_opening_from_token_a(1, Q64_64_ONE, FEE_BPS).unwrap_err();
|
|
assert!(matches!(too_small, IntentError::Quote { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn amounts_above_javascript_integer_range_remain_exact() {
|
|
let amount_a = 1_u128 << 80;
|
|
let amount_b = amount_a.checked_mul(2).unwrap();
|
|
let prepared =
|
|
validate_explicit_opening_pair(amount_a, amount_b, Q64_64_ONE * 2, FEE_BPS).unwrap();
|
|
assert_eq!(prepared.token_a_amount, amount_a);
|
|
assert_eq!(prepared.token_b_amount, amount_b);
|
|
assert_eq!(prepared.quote.pool.reserve_a, amount_a);
|
|
assert_eq!(prepared.quote.pool.reserve_b, amount_b);
|
|
}
|
|
|
|
#[test]
|
|
fn caller_and_stored_order_mapping_is_lossless() {
|
|
assert_eq!(
|
|
caller_amounts_to_stored(PairOrder::Stored, 11, 22),
|
|
(11, 22)
|
|
);
|
|
assert_eq!(
|
|
caller_amounts_to_stored(PairOrder::Reversed, 11, 22),
|
|
(22, 11)
|
|
);
|
|
assert_eq!(
|
|
stored_amounts_to_caller(PairOrder::Reversed, 22, 11),
|
|
(11, 22)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn caller_opening_intent_maps_both_token_orders_without_host_math() {
|
|
let lower = AccountId::new([1; 32]);
|
|
let higher = AccountId::new([2; 32]);
|
|
let desired_price = Q64_64_ONE.checked_mul(2).unwrap();
|
|
|
|
let reversed = prepare_caller_opening_pair(
|
|
lower,
|
|
higher,
|
|
desired_price,
|
|
FEE_BPS,
|
|
OpeningLiquidityIntent::FirstAmount(4_000),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(reversed.caller_order(), PairOrder::Reversed);
|
|
assert_eq!(reversed.first_amount(), 4_000);
|
|
assert_eq!(reversed.second_amount(), 2_000);
|
|
assert_eq!(reversed.stored().token_a_amount, 2_000);
|
|
assert_eq!(reversed.stored().token_b_amount, 4_000);
|
|
|
|
let stored = prepare_caller_opening_pair(
|
|
higher,
|
|
lower,
|
|
desired_price,
|
|
FEE_BPS,
|
|
OpeningLiquidityIntent::Explicit {
|
|
first_amount: 2_000,
|
|
second_amount: 4_000,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(stored.caller_order(), PairOrder::Stored);
|
|
assert_eq!(stored.first_amount(), 2_000);
|
|
assert_eq!(stored.second_amount(), 4_000);
|
|
|
|
assert_eq!(
|
|
prepare_caller_opening_pair(
|
|
lower,
|
|
lower,
|
|
desired_price,
|
|
FEE_BPS,
|
|
OpeningLiquidityIntent::Minimum,
|
|
),
|
|
Err(IntentError::IdenticalTokenDefinitions)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pool_spot_change_is_directional_exact_and_floored_once() {
|
|
let before = pool(10_000, 20_000);
|
|
let quote = program_quote::preview_swap_exact_input(
|
|
&before,
|
|
before.reserve_a,
|
|
before.reserve_b,
|
|
SwapDirection::AToB,
|
|
100,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(quote.pool.reserve_a, 10_100);
|
|
assert_eq!(quote.pool.reserve_b, 19_804);
|
|
assert_eq!(pool_spot_change_bps(&before, "e).unwrap(), 199);
|
|
|
|
let large_quote = program_quote::preview_swap_exact_input(
|
|
&before,
|
|
before.reserve_a,
|
|
before.reserve_b,
|
|
SwapDirection::AToB,
|
|
9_000,
|
|
)
|
|
.unwrap();
|
|
assert!(pool_spot_change_bps(&before, &large_quote).unwrap() > 10_000);
|
|
}
|
|
|
|
#[test]
|
|
fn pool_spot_change_handles_reserves_above_javascript_range() {
|
|
let scale = 1_u128 << 60;
|
|
let reserve_a = scale.checked_mul(10).unwrap();
|
|
let reserve_b = scale.checked_mul(20).unwrap();
|
|
let before = pool(reserve_a, reserve_b);
|
|
let quote = program_quote::preview_swap_exact_input(
|
|
&before,
|
|
before.reserve_a,
|
|
before.reserve_b,
|
|
SwapDirection::BToA,
|
|
scale,
|
|
)
|
|
.unwrap();
|
|
|
|
let change = pool_spot_change_bps(&before, "e).unwrap();
|
|
assert!(change > 0);
|
|
assert!(change <= 10_000);
|
|
}
|
|
|
|
#[test]
|
|
fn pool_spot_change_rejects_zero_directional_reserve() {
|
|
let valid_before = pool(10_000, 20_000);
|
|
let quote = program_quote::preview_swap_exact_input(
|
|
&valid_before,
|
|
valid_before.reserve_a,
|
|
valid_before.reserve_b,
|
|
SwapDirection::AToB,
|
|
100,
|
|
)
|
|
.unwrap();
|
|
let zero_before = pool(0, 20_000);
|
|
|
|
assert_eq!(
|
|
pool_spot_change_bps(&zero_before, "e),
|
|
Err(IntentError::ZeroDirectionalReserve)
|
|
);
|
|
assert_eq!(
|
|
IntentError::ZeroDirectionalReserve.code(),
|
|
"zero_directional_reserve"
|
|
);
|
|
}
|