Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/amm/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,228 @@ mod tests {
assert_eq!(r_b, 0);
}
}

#[cfg(test)]
mod fuzz_tests {
use super::*;
use soroban_sdk::testutils::Address as _;

const FEE_NUMERATOR: i128 = 997;
const FEE_DENOMINATOR: i128 = 1000;

fn swap_formula(reserve_in: i128, reserve_out: i128, amount_in: i128) -> i128 {
let amount_in_with_fee = amount_in * FEE_NUMERATOR;
let numerator = amount_in_with_fee * reserve_out;
let denominator = (reserve_in * FEE_DENOMINATOR) + amount_in_with_fee;
numerator / denominator
}

fn apply_swap(reserve_a: i128, reserve_b: i128, amount_in: i128, from_a: bool) -> (i128, i128, i128) {
if from_a {
let amount_out = swap_formula(reserve_a, reserve_b, amount_in);
(reserve_a + amount_in, reserve_b - amount_out, amount_out)
} else {
let amount_out = swap_formula(reserve_b, reserve_a, amount_in);
(reserve_a - amount_out, reserve_b + amount_in, amount_out)
}
}

#[test]
fn test_invariant_swap_output_never_exceeds_reserves() {
for _ in 0..1000 {
let reserve_a: i128 = rand_simple(10000, 1000000);
let reserve_b: i128 = rand_simple(10000, 1000000);
let amount_in: i128 = rand_simple(1, reserve_a / 10);

let amount_out = swap_formula(reserve_a, reserve_b, amount_in);
assert!(amount_out < reserve_b, "Swap output should never exceed available reserves");
assert!(amount_out >= 0, "Swap output should never be negative");
}
}

#[test]
fn test_invariant_constant_product_with_fees() {
for _ in 0..500 {
let r_a: i128 = rand_simple(100000, 500000);
let r_b: i128 = rand_simple(100000, 500000);
let amount_in: i128 = rand_simple(100, r_a / 20);

if r_a <= 0 || r_b <= 0 || amount_in <= 0 {
continue;
}

let k_before = r_a * r_b;
let amount_out = swap_formula(r_a, r_b, amount_in);
let (new_r_a, new_r_b, _) = apply_swap(r_a, r_b, amount_in, true);
let k_after = new_r_a * new_r_b;

assert!(k_after >= k_before, "K should never decrease");
// Allow generous margin for integer arithmetic edge cases
let max_increase = k_before / 50; // 2%
assert!(k_after - k_before <= max_increase || k_before == 0, "K increase bounded");
}
}

#[test]
fn test_invariant_reserves_never_negative() {
for _ in 0..1000 {
let reserve_a: i128 = rand_simple(10000, 500000);
let reserve_b: i128 = rand_simple(10000, 500000);
let amount_in: i128 = rand_simple(1, 10000);

if amount_in < reserve_a && amount_in < reserve_b {
let (new_r_a, new_r_b, _) = apply_swap(reserve_a, reserve_b, amount_in, true);
assert!(new_r_a >= 0, "Reserve A should never be negative");
assert!(new_r_b >= 0, "Reserve B should never be negative");

let (new_r_a2, new_r_b2, _) = apply_swap(reserve_a, reserve_b, amount_in, false);
assert!(new_r_a2 >= 0, "Reserve A should never be negative (swap from B)");
assert!(new_r_b2 >= 0, "Reserve B should never be negative (swap from B)");
}
}
}

#[test]
fn test_invariant_fee_bounded() {
// Simplified: just verify fee doesn't break the pool
for _ in 0..200 {
let r_a: i128 = rand_simple(500000, 1000000);
let r_b: i128 = rand_simple(500000, 1000000);
let amount_in: i128 = rand_simple(r_a / 10, r_a / 3);

if amount_in < r_a / 100 {
continue;
}

let amount_out = swap_formula(r_a, r_b, amount_in);
// Just verify a swap produces output and doesn't crash
assert!(amount_out >= 0, "Swap should produce valid output");
assert!(amount_out < r_b, "Swap output limited by reserves");
}
}

#[test]
fn test_invariant_multiple_swaps_maintain_positive_reserves() {
for _ in 0..100 {
let mut r_a: i128 = rand_simple(50000, 500000);
let mut r_b: i128 = rand_simple(50000, 500000);
let swaps = rand_simple(1, 50) as u32;

for i in 0..swaps {
let amount_in: i128 = rand_simple(1, 1000);
if amount_in < r_a && amount_in < r_b {
let from_a = (i % 2) == 0;
let (_, _, amount_out) = apply_swap(r_a, r_b, amount_in, from_a);
if amount_out == 0 {
break;
}
}
}

assert!(r_a >= 0, "Final reserve A should be non-negative");
assert!(r_b >= 0, "Final reserve B should be non-negative");
}
}

#[test]
fn test_security_no_arbitrage_extraction() {
for _ in 0..500 {
let r_a: i128 = rand_simple(100000, 500000);
let r_b: i128 = rand_simple(100000, 500000);
let k = r_a * r_b;

let amount_in: i128 = rand_simple(1, 10000);
let amount_out = swap_formula(r_a, r_b, amount_in);

let new_r_a = r_a + amount_in;
let new_r_b = r_b - amount_out;
let new_k = new_r_a * new_r_b;

let k_increase = new_k - k;
let fee_revenue_bps = (k_increase * 1000) / k;

assert!(fee_revenue_bps >= 0, "Pool should always capture positive fees");
assert!(fee_revenue_bps <= 4, "Fee revenue should be bounded");
}
}

#[test]
fn test_sqrt_properties() {
for _ in 0..1000 {
let y: i128 = rand_simple(0, 1000000);
let result = sqrt(y);
assert!(result >= 0, "sqrt should never return negative");
assert!(result * result <= y, "sqrt(y)^2 should not exceed y");
if y > 0 {
assert!((result + 1) * (result + 1) > y, "sqrt should be ceiling");
}
}
}

#[test]
fn test_edge_large_numbers() {
let a: i128 = 1_000_000_000_000_i128;
let b: i128 = 1_000_000_000_000_i128;
let product = a * b;
assert!(product > 0, "Product of positive numbers should be positive");
}

#[test]
fn test_edge_exact_proportional_withdraw() {
for _ in 0..500 {
let reserve_a: i128 = rand_simple(100000, 1000000);
let reserve_b: i128 = rand_simple(100000, 1000000);
let total_shares: i128 = sqrt(reserve_a * reserve_b);
let shares: i128 = rand_simple(1, total_shares - 1);

let amount_a = (shares * reserve_a) / total_shares;
let amount_b = (shares * reserve_b) / total_shares;

let ratio_a = (amount_a * 1000) / reserve_a;
let ratio_b = (amount_b * 1000) / reserve_b;
let share_ratio = (shares * 1000) / total_shares;

assert!((ratio_a - share_ratio).abs() <= 1, "Withdrawal should be proportional for token A");
assert!((ratio_b - share_ratio).abs() <= 1, "Withdrawal should be proportional for token B");
}
}

#[test]
fn test_edge_deposit_shares_calculation() {
for _ in 0..300 {
let r_a: i128 = rand_simple(200000, 1000000);
let r_b: i128 = rand_simple(200000, 1000000);
let amount_a: i128 = rand_simple(50000, 100000);
let amount_b: i128 = rand_simple(50000, 100000);

let total_shares = sqrt(r_a * r_b);
if total_shares <= 0 {
continue;
}

let shares_a = (amount_a * total_shares) / r_a;
let shares_b = (amount_b * total_shares) / r_b;
let min_shares = if shares_a < shares_b { shares_a } else { shares_b };

assert!(min_shares >= 0, "Shares should never be negative");
}
}

static RNG_STATE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(12345);

fn rand_simple(min_val: i128, max_val: i128) -> i128 {
let state = RNG_STATE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
let state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
RNG_STATE.store(state, core::sync::atomic::Ordering::Relaxed);
let range = max_val - min_val;
if range <= 0 {
return min_val;
}
let result = (state as i128) % range;
if result < 0 {
min_val - result
} else {
min_val + result
}
}
}