mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 22:21:16 +00:00
Add validated shared quote orchestration, canonical planners for every guest instruction, and exact RISC Zero serialization. Expose integer-only slippage preparation and lossless JSON/C adapters without runtime deployment identity checks.
340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
use amm_program::{
|
|
core::{
|
|
spot_price_q64_64, PoolDefinition, FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30,
|
|
FEE_TIER_BPS_5, MINIMUM_LIQUIDITY, SUPPORTED_FEE_TIERS,
|
|
},
|
|
quote::{self, PairOrder, PoolUpdate, QuoteErrorCode, SwapDirection},
|
|
};
|
|
use nssa_core::account::AccountId;
|
|
use twap_oracle_core::OBSERVATIONS_CAPACITY;
|
|
|
|
fn token_a_id() -> AccountId {
|
|
AccountId::new([1; 32])
|
|
}
|
|
|
|
fn token_b_id() -> AccountId {
|
|
AccountId::new([2; 32])
|
|
}
|
|
|
|
fn pool() -> PoolDefinition {
|
|
PoolDefinition {
|
|
definition_token_a_id: token_a_id(),
|
|
definition_token_b_id: token_b_id(),
|
|
vault_a_id: AccountId::new([3; 32]),
|
|
vault_b_id: AccountId::new([4; 32]),
|
|
liquidity_pool_id: AccountId::new([5; 32]),
|
|
liquidity_pool_supply: 2_000,
|
|
reserve_a: 1_000,
|
|
reserve_b: 500,
|
|
fees: FEE_TIER_BPS_30,
|
|
}
|
|
}
|
|
|
|
fn assert_pool_update(
|
|
update: PoolUpdate,
|
|
liquidity_pool_supply: u128,
|
|
reserve_a: u128,
|
|
reserve_b: u128,
|
|
) {
|
|
assert_eq!(update.liquidity_pool_supply, liquidity_pool_supply);
|
|
assert_eq!(update.reserve_a, reserve_a);
|
|
assert_eq!(update.reserve_b, reserve_b);
|
|
assert_eq!(
|
|
update.spot_price_q64_64,
|
|
spot_price_q64_64(reserve_a, reserve_b)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_pool_quotes_locked_and_user_liquidity() {
|
|
let quoted = quote::create_pool(4_000, 9_000, FEE_TIER_BPS_30)
|
|
.expect("valid initial liquidity should quote");
|
|
|
|
assert_pool_update(quoted.pool, 6_000, 4_000, 9_000);
|
|
assert_eq!(quoted.locked_liquidity, MINIMUM_LIQUIDITY);
|
|
assert_eq!(quoted.user_liquidity, 5_000);
|
|
}
|
|
|
|
#[test]
|
|
fn create_pool_quote_preserves_spot_price_saturation() {
|
|
let quoted = quote::create_pool(1, u128::MAX, FEE_TIER_BPS_30)
|
|
.expect("spot-price range overflow should saturate, not reject the amount quote");
|
|
|
|
assert_eq!(quoted.pool.spot_price_q64_64, u128::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn supported_fee_tiers_are_exposed_as_a_slice() {
|
|
let tiers: &[u128] = SUPPORTED_FEE_TIERS;
|
|
|
|
assert_eq!(
|
|
tiers,
|
|
&[
|
|
FEE_TIER_BPS_1,
|
|
FEE_TIER_BPS_5,
|
|
FEE_TIER_BPS_30,
|
|
FEE_TIER_BPS_100,
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_liquidity_quotes_program_rounding_and_post_pool() {
|
|
let quoted = quote::add_liquidity(&pool(), 1_000, 500, 400, 100, 399)
|
|
.expect("valid proportional deposit should quote");
|
|
|
|
assert_eq!(quoted.actual_amount_a, 200);
|
|
assert_eq!(quoted.actual_amount_b, 100);
|
|
assert_eq!(quoted.liquidity_to_mint, 400);
|
|
assert_pool_update(quoted.pool, 2_400, 1_200, 600);
|
|
}
|
|
|
|
#[test]
|
|
fn preview_helpers_return_amounts_before_client_slippage_policy() {
|
|
let add = quote::preview_add_liquidity(&pool(), 1_000, 500, 400, 100)
|
|
.expect("valid add should preview");
|
|
let remove =
|
|
quote::preview_remove_liquidity(&pool(), 1_000, 500).expect("valid removal should preview");
|
|
let exact_input =
|
|
quote::preview_swap_exact_input(&pool(), 1_000, 500, SwapDirection::AToB, 100)
|
|
.expect("valid exact-input trade should preview");
|
|
let exact_output =
|
|
quote::preview_swap_exact_output(&pool(), 1_000, 500, SwapDirection::AToB, 45)
|
|
.expect("valid exact-output trade should preview");
|
|
|
|
assert_eq!(add.liquidity_to_mint, 400);
|
|
assert_eq!(remove.withdraw_amount_a, 250);
|
|
assert_eq!(exact_input.amount_out, 45);
|
|
assert_eq!(exact_output.amount_in, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_liquidity_quotes_program_rounding_and_post_pool() {
|
|
let quoted = quote::remove_liquidity(&pool(), 1_000, 500, 250, 125)
|
|
.expect("valid proportional withdrawal should quote");
|
|
|
|
assert_eq!(quoted.withdraw_amount_a, 250);
|
|
assert_eq!(quoted.withdraw_amount_b, 125);
|
|
assert_eq!(quoted.liquidity_to_burn, 500);
|
|
assert_pool_update(quoted.pool, 1_500, 750, 375);
|
|
}
|
|
|
|
#[test]
|
|
fn exact_input_and_output_quotes_share_the_same_boundary() {
|
|
let exact_input = quote::swap_exact_input(&pool(), 1_000, 500, SwapDirection::AToB, 100, 45)
|
|
.expect("valid exact-input trade should quote");
|
|
let exact_output = quote::swap_exact_output(&pool(), 1_000, 500, SwapDirection::AToB, 45, 100)
|
|
.expect("valid exact-output trade should quote");
|
|
|
|
assert_eq!(exact_input, exact_output);
|
|
assert_eq!(exact_input.direction, SwapDirection::AToB);
|
|
assert_eq!(exact_input.amount_in, 100);
|
|
assert_eq!(exact_input.effective_amount_in, 99);
|
|
assert_eq!(exact_input.fee_amount, 1);
|
|
assert_eq!(exact_input.amount_out, 45);
|
|
assert_pool_update(exact_input.pool, 2_000, 1_100, 455);
|
|
}
|
|
|
|
#[test]
|
|
fn reverse_swap_quote_keeps_pool_updates_in_stored_order() {
|
|
let quoted = quote::swap_exact_input(&pool(), 1_000, 500, SwapDirection::BToA, 100, 165)
|
|
.expect("valid reverse trade should quote");
|
|
|
|
assert_eq!(quoted.direction, SwapDirection::BToA);
|
|
assert_eq!(quoted.amount_in, 100);
|
|
assert_eq!(quoted.effective_amount_in, 99);
|
|
assert_eq!(quoted.fee_amount, 1);
|
|
assert_eq!(quoted.amount_out, 165);
|
|
assert_pool_update(quoted.pool, 2_000, 835, 600);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_reserves_reports_donations_and_post_pool() {
|
|
let quoted = quote::sync_reserves(&pool(), 1_100, 550)
|
|
.expect("vault donations above reserves should quote");
|
|
|
|
assert_eq!(quoted.donated_amount_a, 100);
|
|
assert_eq!(quoted.donated_amount_b, 50);
|
|
assert_pool_update(quoted.pool, 2_000, 1_100, 550);
|
|
}
|
|
|
|
#[test]
|
|
fn pair_and_swap_direction_follow_stored_pool_order() {
|
|
let pool = pool();
|
|
|
|
assert_eq!(
|
|
quote::pair_order(&pool, token_a_id(), token_b_id()),
|
|
Ok(PairOrder::Stored)
|
|
);
|
|
assert_eq!(
|
|
quote::pair_order(&pool, token_b_id(), token_a_id()),
|
|
Ok(PairOrder::Reversed)
|
|
);
|
|
assert_eq!(
|
|
quote::swap_direction(&pool, token_a_id()),
|
|
Ok(SwapDirection::AToB)
|
|
);
|
|
assert_eq!(
|
|
quote::swap_direction(&pool, token_b_id()),
|
|
Ok(SwapDirection::BToA)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_price_quote_uses_pool_assets_and_spot_price() {
|
|
let window_duration = u64::from(OBSERVATIONS_CAPACITY);
|
|
let result = quote::create_oracle_price_account(&pool(), window_duration)
|
|
.expect("valid pool and window should quote");
|
|
|
|
assert_eq!(result.base_asset, token_a_id());
|
|
assert_eq!(result.quote_asset, token_b_id());
|
|
assert_eq!(result.initial_price_q64_64, spot_price_q64_64(1_000, 500));
|
|
assert_eq!(result.window_duration, window_duration);
|
|
}
|
|
|
|
#[test]
|
|
fn quote_errors_expose_stable_machine_codes() {
|
|
let error = quote::add_liquidity(&pool(), 1_000, 500, 400, 100, 401)
|
|
.expect_err("minimum above minted liquidity must fail");
|
|
|
|
assert_eq!(error.kind(), QuoteErrorCode::MintedLiquidityBelowMinimum);
|
|
assert_eq!(error.code(), "minted_liquidity_below_minimum");
|
|
assert_eq!(
|
|
error.message(),
|
|
"Payable LP is less than provided minimum LP amount"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quote_error_codes_have_stable_strings() {
|
|
let cases = [
|
|
(QuoteErrorCode::ArithmeticOverflow, "arithmetic_overflow"),
|
|
(QuoteErrorCode::DepositAmountZero, "deposit_amount_zero"),
|
|
(
|
|
QuoteErrorCode::EffectiveSwapInputZero,
|
|
"effective_swap_input_zero",
|
|
),
|
|
(
|
|
QuoteErrorCode::ExactOutputExceedsReserve,
|
|
"exact_output_exceeds_reserve",
|
|
),
|
|
(QuoteErrorCode::ExactOutputZero, "exact_output_zero"),
|
|
(
|
|
QuoteErrorCode::InitialLiquidityTooLow,
|
|
"initial_liquidity_too_low",
|
|
),
|
|
(
|
|
QuoteErrorCode::InputTokenNotInPool,
|
|
"input_token_not_in_pool",
|
|
),
|
|
(
|
|
QuoteErrorCode::InvalidLiquidityAccount,
|
|
"invalid_liquidity_account",
|
|
),
|
|
(
|
|
QuoteErrorCode::LiquiditySupplyBelowMinimum,
|
|
"liquidity_supply_below_minimum",
|
|
),
|
|
(QuoteErrorCode::MaximumDepositZero, "maximum_deposit_zero"),
|
|
(
|
|
QuoteErrorCode::MinimumLiquidityZero,
|
|
"minimum_liquidity_zero",
|
|
),
|
|
(
|
|
QuoteErrorCode::MinimumWithdrawalZero,
|
|
"minimum_withdrawal_zero",
|
|
),
|
|
(
|
|
QuoteErrorCode::MintedLiquidityBelowMinimum,
|
|
"minted_liquidity_below_minimum",
|
|
),
|
|
(QuoteErrorCode::MintedLiquidityZero, "minted_liquidity_zero"),
|
|
(QuoteErrorCode::OraclePriceZero, "oracle_price_zero"),
|
|
(
|
|
QuoteErrorCode::OracleWindowTooShort,
|
|
"oracle_window_too_short",
|
|
),
|
|
(
|
|
QuoteErrorCode::PoolContainsOnlyLockedLiquidity,
|
|
"pool_contains_only_locked_liquidity",
|
|
),
|
|
(
|
|
QuoteErrorCode::RemoveAmountExceedsUnlockedLiquidity,
|
|
"remove_amount_exceeds_unlocked_liquidity",
|
|
),
|
|
(
|
|
QuoteErrorCode::RemoveAmountExceedsUserBalance,
|
|
"remove_amount_exceeds_user_balance",
|
|
),
|
|
(
|
|
QuoteErrorCode::RemoveLiquidityAmountZero,
|
|
"remove_liquidity_amount_zero",
|
|
),
|
|
(
|
|
QuoteErrorCode::RequiredInputExceedsMaximum,
|
|
"required_input_exceeds_maximum",
|
|
),
|
|
(QuoteErrorCode::ReserveAZero, "reserve_a_zero"),
|
|
(QuoteErrorCode::ReserveZero, "reserve_zero"),
|
|
(
|
|
QuoteErrorCode::SwapOutputBelowMinimum,
|
|
"swap_output_below_minimum",
|
|
),
|
|
(QuoteErrorCode::SwapOutputZero, "swap_output_zero"),
|
|
(QuoteErrorCode::TokenAAmountZero, "token_a_amount_zero"),
|
|
(QuoteErrorCode::TokenBAmountZero, "token_b_amount_zero"),
|
|
(QuoteErrorCode::TokenPairNotInPool, "token_pair_not_in_pool"),
|
|
(QuoteErrorCode::UnsupportedFeeTier, "unsupported_fee_tier"),
|
|
(
|
|
QuoteErrorCode::VaultABalanceBelowReserve,
|
|
"vault_a_balance_below_reserve",
|
|
),
|
|
(
|
|
QuoteErrorCode::VaultBBalanceBelowReserve,
|
|
"vault_b_balance_below_reserve",
|
|
),
|
|
(
|
|
QuoteErrorCode::WithdrawalABelowMinimum,
|
|
"withdrawal_a_below_minimum",
|
|
),
|
|
(
|
|
QuoteErrorCode::WithdrawalBBelowMinimum,
|
|
"withdrawal_b_below_minimum",
|
|
),
|
|
];
|
|
|
|
assert_eq!(cases.len(), 33);
|
|
for (kind, expected) in cases {
|
|
assert_eq!(kind.as_str(), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_quotes_apply_instruction_slippage_guards() {
|
|
let add = quote::add_liquidity(&pool(), 1_000, 500, 400, 100, 401)
|
|
.expect_err("minimum LP above quote must fail");
|
|
let remove = quote::remove_liquidity(&pool(), 1_000, 500, 251, 125)
|
|
.expect_err("minimum token A above quote must fail");
|
|
let exact_input = quote::swap_exact_input(&pool(), 1_000, 500, SwapDirection::AToB, 100, 46)
|
|
.expect_err("minimum output above quote must fail");
|
|
let exact_output = quote::swap_exact_output(&pool(), 1_000, 500, SwapDirection::AToB, 45, 99)
|
|
.expect_err("maximum input below quote must fail");
|
|
|
|
assert_eq!(add.code(), "minted_liquidity_below_minimum");
|
|
assert_eq!(remove.code(), "withdrawal_a_below_minimum");
|
|
assert_eq!(exact_input.code(), "swap_output_below_minimum");
|
|
assert_eq!(exact_output.code(), "required_input_exceeds_maximum");
|
|
}
|
|
|
|
#[test]
|
|
fn arithmetic_overflow_is_returned_instead_of_panicking() {
|
|
let mut extreme_pool = pool();
|
|
extreme_pool.reserve_a = u128::MAX;
|
|
extreme_pool.reserve_b = 1;
|
|
|
|
let error = quote::add_liquidity(&extreme_pool, u128::MAX, 1, u128::MAX, u128::MAX, 1)
|
|
.expect_err("unrepresentable ideal amount must fail");
|
|
|
|
assert_eq!(error.code(), "arithmetic_overflow");
|
|
}
|