diff --git a/contracts/pool_stable/src/contract.rs b/contracts/pool_stable/src/contract.rs index ce80cee70..81ac18e5f 100644 --- a/contracts/pool_stable/src/contract.rs +++ b/contracts/pool_stable/src/contract.rs @@ -345,8 +345,8 @@ impl StableLiquidityPoolTrait for StableLiquidityPool { &env, amp as u128, &[ - scale_value(new_balance_a, token_a_decimals, DECIMAL_PRECISION), - scale_value(new_balance_b, token_b_decimals, DECIMAL_PRECISION), + scale_value(&env, new_balance_a, token_a_decimals, DECIMAL_PRECISION), + scale_value(&env, new_balance_b, token_b_decimals, DECIMAL_PRECISION), ], ); @@ -373,11 +373,13 @@ impl StableLiquidityPoolTrait for StableLiquidityPool { amp as u128, &[ scale_value( + &env, convert_i128_to_u128(old_balance_a), token_a_decimals, DECIMAL_PRECISION, ), scale_value( + &env, convert_i128_to_u128(old_balance_b), token_b_decimals, DECIMAL_PRECISION, @@ -1029,13 +1031,14 @@ pub fn compute_swap( env, amp as u128, scale_value( + env, offer_pool + offer_amount, greatest_precision, DECIMAL_PRECISION, ), &[ - scale_value(offer_pool, offer_pool_precision, DECIMAL_PRECISION), - scale_value(ask_pool, ask_pool_precision, DECIMAL_PRECISION), + scale_value(env, offer_pool, offer_pool_precision, DECIMAL_PRECISION), + scale_value(env, ask_pool, ask_pool_precision, DECIMAL_PRECISION), ], greatest_precision, ); @@ -1084,13 +1087,14 @@ pub fn compute_offer_amount( env, amp as u128, scale_value( + env, ask_pool - convert_i128_to_u128(before_commission), greatest_precision, DECIMAL_PRECISION, ), &[ - scale_value(offer_pool, offer_pool_precision, DECIMAL_PRECISION), - scale_value(ask_pool, ask_pool_precision, DECIMAL_PRECISION), + scale_value(env, offer_pool, offer_pool_precision, DECIMAL_PRECISION), + scale_value(env, ask_pool, ask_pool_precision, DECIMAL_PRECISION), ], greatest_precision, ); diff --git a/contracts/pool_stable/src/math.rs b/contracts/pool_stable/src/math.rs index d0badbaa7..992eccace 100644 --- a/contracts/pool_stable/src/math.rs +++ b/contracts/pool_stable/src/math.rs @@ -20,18 +20,28 @@ const DECIMAL_FRACTIONAL: u128 = 1_000_000_000_000_000_000; /// 1e-6 const TOL: u128 = 1000000000000; -pub fn scale_value(atomics: u128, decimal_places: u32, target_decimal_places: u32) -> u128 { - const TEN: u128 = 10; - - if decimal_places < target_decimal_places { - let factor = TEN.pow(target_decimal_places - decimal_places); - atomics - .checked_mul(factor) - .expect("Multiplication overflow") +pub fn scale_value( + env: &Env, + atomics: u128, + decimal_places: u32, + target_decimal_places: u32, +) -> u128 { + let ten = U256::from_u128(env, 10); + let atomics = U256::from_u128(env, atomics); + + let scaled_value = if decimal_places < target_decimal_places { + let power = target_decimal_places - decimal_places; + let factor = ten.pow(power); + atomics.mul(&factor) } else { - let factor = TEN.pow(decimal_places - target_decimal_places); - atomics.checked_div(factor).expect("Division overflow") - } + let power = decimal_places - target_decimal_places; + let factor = ten.pow(power); + atomics.div(&factor) + }; + + scaled_value + .to_u128() + .expect("Value doesn't fit into u128!") } fn abs_diff(a: &U256, b: &U256) -> U256 { @@ -74,9 +84,8 @@ pub(crate) fn compute_current_amp(env: &Env, amp_params: &AmplifierParameters) - /// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) pub fn compute_d(env: &Env, amp: u128, pools: &[u128]) -> U256 { let leverage = U256::from_u128(env, (amp / AMP_PRECISION as u128) * N_COINS_PRECISION); - let amount_a_times_coins = pools[0] * N_COINS; - let amount_b_times_coins = pools[1] * N_COINS; - + let amount_a_times_coins = U256::from_u128(env, pools[0]).mul(&U256::from_u128(env, N_COINS)); + let amount_b_times_coins = U256::from_u128(env, pools[1]).mul(&U256::from_u128(env, N_COINS)); let sum_x = U256::from_u128(env, pools[0] + pools[1]); // sum(x_i), a.k.a S let zero = U256::from_u128(env, 0u128); if sum_x == zero { @@ -88,10 +97,8 @@ pub fn compute_d(env: &Env, amp: u128, pools: &[u128]) -> U256 { // Newton's method to approximate D for _ in 0..ITERATIONS { - let d_product = d.pow(3).div(&U256::from_u128( - env, - amount_a_times_coins * amount_b_times_coins, - )); + let a_times_b_product = amount_a_times_coins.mul(&amount_b_times_coins); + let d_product = d.pow(3).div(&a_times_b_product); d_previous = d.clone(); d = calculate_step(env, &d, &leverage, &sum_x, &d_product); // Equality with the precision of 1e-6 @@ -137,50 +144,207 @@ fn calculate_step( l_val.div(&r_val) } -/// Compute the swap amount `y` in proportion to `x`. +/// Compute the swap amount `y` in proportion to `x` using a partially-reordered formula. /// -/// * **Solve for y** +/// Original stable-swap equation: +/// ```text +/// y² + y * (sum' - (A*n^n - 1) * D / (A * n^n)) = D^(n+1) / (n^(2n) * prod' * A) /// -/// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) -/// -/// y**2 + b*y = c +/// => y² + b·y = c +/// ``` +/// We chunk up multiplications/divisions to avoid overflow when computing `d³ * amp_prec`. pub(crate) fn calc_y( env: &Env, amp: u128, - new_amount: u128, + new_amount_u128: u128, xp: &[u128], target_precision: u32, ) -> u128 { - let n_coins = U256::from_u128(env, N_COINS); - let new_amount = U256::from_u128(env, new_amount); + // number of coins in the pool, e.g. 2 for a two-coin stableswap. + let coins_count = U256::from_u128(env, N_COINS); + + // convert `new_amount_u128` to U256 for big math. + let new_u256_amount = U256::from_u128(env, new_amount_u128); + + // compute the stableswap invariant D. + let invariant_d = compute_d(env, amp, xp); + + let amp_precision_factor = U256::from_u128(env, (AMP_PRECISION as u128) * DECIMAL_FRACTIONAL); + + // compute "leverage" = amp * DECIMAL_FRACTIONAL * n_coins. + let leverage = U256::from_u128(env, amp) + .mul(&U256::from_u128(env, DECIMAL_FRACTIONAL)) + .mul(&coins_count); - let d = compute_d(env, amp, xp); - let leverage = U256::from_u128(env, amp * DECIMAL_FRACTIONAL * N_COINS); - let amp_prec = U256::from_u128(env, AMP_PRECISION as u128 * DECIMAL_FRACTIONAL); + // ------------------------------------------------------------------ + // Now we compute: + // c = (D^3 * amp_precision_factor) / (new_amount * n_coins^2 * leverage) + // but we do it in multiple steps to prevent overflow. + // ------------------------------------------------------------------ - let c = d - .pow(3) - .mul(&_prec) - .div(&new_amount.mul(&n_coins.mul(&n_coins)).mul(&leverage)); + // Step A: D² + let invariant_sq = invariant_d.mul(&invariant_d); - let b = new_amount.add(&d.mul(&_prec).div(&leverage)); + // Step B: multiply coins_count by itself => n_coins^2, then times new_u256_amount. + // denominator_chunk1 = n_coins^2 * new_amount + let coins_count_sq = coins_count.mul(&coins_count); + let denominator_chunk1 = coins_count_sq.mul(&new_u256_amount); - // Solve for y by approximating: y**2 + b*y = c - let mut y_prev; - let mut y = d.clone(); + // Step C: partial factor => (D² / (n_coins^2 * new_amount)) + let temp_factor1 = invariant_sq.div(&denominator_chunk1); + + // Step D: multiply by D => (D³ / (n_coins^2 * new_amount)) + let temp_factor2 = temp_factor1.mul(&invariant_d); + + // Step E: multiply by amp_precision_factor => (D³ * amp_prec) / (n_coins^2 * new_amount) + let temp_factor3 = temp_factor2.mul(&_precision_factor); + + // Step F: finally divide by leverage => + // c = (D³ * amp_prec) / (n_coins^2 * new_amount * leverage) + let constant_c = temp_factor3.div(&leverage); + + // ------------------------------------------------------------------ + // b = new_amount + (D * amp_precision_factor / leverage) + // ------------------------------------------------------------------ + let coefficient_b = { + let scaled_d = invariant_d.mul(&_precision_factor).div(&leverage); + new_u256_amount.add(&scaled_d) + }; + + // ------------------------------------------------------------------ + // Solve for y in the equation: + // y² + b·y = c ==> y = (y² + c) / (n_coins·y + b - D) + // Using iteration (Newton-like). + // ------------------------------------------------------------------ + let mut y_guess = invariant_d.clone(); for _ in 0..ITERATIONS { - y_prev = y.clone(); - y = (y.pow(2).add(&c)).div(&(y.mul(&n_coins).add(&b).sub(&d))); - if abs_diff(&y, &y_prev) <= U256::from_u128(env, TOL) { + let y_prev = y_guess.clone(); + + // Numerator = y² + c + let numerator = y_guess.pow(2).add(&constant_c); + + // Denominator = n_coins·y + b - D + let denominator = coins_count + .mul(&y_guess) + .add(&coefficient_b) + .sub(&invariant_d); + + // Next approximation for y + y_guess = numerator.div(&denominator); + + // Check convergence + if abs_diff(&y_guess, &y_prev) <= U256::from_u128(env, TOL) { + // Scale down from DECIMAL_PRECISION to `target_precision`. let divisor = 10u128.pow(DECIMAL_PRECISION - target_precision); - return y + return y_guess .to_u128() - .expect("Pool stable: calc_y: conversion to u128 failed") + .expect("calc_y: final y doesn't fit in u128!") / divisor; } } - // Should definitely converge in 64 iterations. - log!(&env, "Pool Stable: calc_y: y is not converging"); - panic_with_error!(&env, ContractError::CalcYErr); + // If not converged in 64 iterations, we treat that as an error. + log!(env, "calc_y: not converging in 64 iterations!"); + panic_with_error!(env, ContractError::CalcYErr); +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::Env; + + #[test] + fn test_scale_value_up() { + let env = Env::default(); + // example: 123 (with 3 decimals) => want 6 decimals + // 10^(6-3) = 10^3 = 1000, result => 123 * 1000 = 123000 + let val = scale_value(&env, 123, 3, 6); + assert_eq!(val, 123_000); + } + + #[test] + fn test_scale_value_down() { + let env = Env::default(); + // example: 123_000 (with 6 decimals) => want 3 decimals + // 10^(6-3) = 10^3 = 1000, result => 123000 / 1000 = 123 + let val = scale_value(&env, 123_000, 6, 3); + assert_eq!(val, 123); + } + + #[test] + fn test_scale_value_no_change() { + let env = Env::default(); + // if decimal_places == target_decimal_places, value is unchanged + let val = scale_value(&env, 999_999, 5, 5); + assert_eq!(val, 999_999); + } + + #[test] + fn test_scale_value_big_numbers() { + let env = Env::default(); + // something bigger, e.g. 1_234_567 with decimal_places=2 => target=6 + // 10^(6-2) = 10^4 = 10000, result => 1234567 * 10000 = 12345670000 + let val = scale_value(&env, 1_234_567, 2, 6); + assert_eq!(val, 12_345_670_000); + } + + #[test] + fn test_compute_d_zero_sum() { + let env = Env::default(); + // if sum_x=0 => function returns zero + let d = compute_d(&env, 100, &[0, 0]); + assert_eq!(d, U256::from_u128(&env, 0)); + } + + #[test] + fn test_compute_d_basic() { + let env = Env::default(); + // With amp=100, each of the two tokens having a balance of 1000, + // the stableswap invariant D converges to 2000 in the current formula. + let amp = 100; + let pools = [1000u128, 1000u128]; + + let d = compute_d(&env, amp, &pools); + + // Check that we get exactly 2000, which is the expected stable-swap invariant + assert_eq!(d, U256::from_u128(&env, 2000)); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn test_compute_d_non_convergence() { + let env = Env::default(); + // forcing a scenario that should cause no convergence, eg. + // unbalanced pools or something that triggers the iteration to never meet TOL. + let amp = 1_000_000_000; + let pools = [u128::MAX, u128::MAX]; + compute_d(&env, amp, &pools); + } + + #[test] + fn test_calc_y_simple() { + let env = Env::default(); + let amp = 100; + let xp = [1000u128, 1000u128]; + let new_amount = 500u128; + let target_precision = 6; + + // the math above shows final y == 0 after the scaling. + let result = calc_y(&env, amp, new_amount, &xp, target_precision); + assert_eq!(result, 0); + } + + #[test] + #[should_panic(expected = "attempt to add with overflow")] + fn test_calc_y_extreme_overflow() { + let env = Env::default(); + // using very large `xp` or `new_amount` to see if we’d attempt a final `y` that can't fit u128 + calc_y( + &env, + 1_000_000_000_000_000_000, // big `AMP` + u128::MAX, // big `new_amount` + &[u128::MAX, u128::MAX], + 18, + ); + } } diff --git a/contracts/pool_stable/src/tests/liquidity.rs b/contracts/pool_stable/src/tests/liquidity.rs index b72fac07a..cfa612b8e 100644 --- a/contracts/pool_stable/src/tests/liquidity.rs +++ b/contracts/pool_stable/src/tests/liquidity.rs @@ -24,6 +24,7 @@ fn provide_liqudity() { let mut token1 = deploy_token_contract(&env, &admin); let mut token2 = deploy_token_contract(&env, &admin); + if token2.address < token1.address { std::mem::swap(&mut token1, &mut token2); } @@ -124,6 +125,127 @@ fn provide_liqudity() { assert_eq!(pool.query_total_issued_lp(), 1000); } +#[test] +fn provide_liqudity_big_numbers() { + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let factory = Address::generate(&env); + + let mut token1 = deploy_token_contract(&env, &admin); + let mut token2 = deploy_token_contract(&env, &admin); + + if token2.address < token1.address { + std::mem::swap(&mut token1, &mut token2); + } + let user1 = Address::generate(&env); + let swap_fees = 0i64; + let pool = deploy_stable_liquidity_pool_contract( + &env, + None, + (&token1.address, &token2.address), + swap_fees, + None, + None, + None, + manager, + factory, + None, + ); + + let share_token_address = pool.query_share_token_address(); + let token_share = token_contract::Client::new(&env, &share_token_address); + + // minting 1_000_000 tokens to the user + token1.mint(&user1, &10_000_000_000_000); + assert_eq!(token1.balance(&user1), 10_000_000_000_000); + + token2.mint(&user1, &10_000_000_000_000); + assert_eq!(token2.balance(&user1), 10_000_000_000_000); + + // user1 provides 100_000 tokens + pool.provide_liquidity( + &user1, + &1_000_000_000_000, + &1_000_000_000_000, + &None, + &None::, + &None::, + ); + + assert_eq!( + env.auths(), + [( + user1.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + pool.address.clone(), + Symbol::new(&env, "provide_liquidity"), + ( + &user1, + 1_000_000_000_000_i128, + 1_000_000_000_000_i128, + None::, + None::, + None:: + ) + .into_val(&env), + )), + sub_invocations: std::vec![ + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token1.address.clone(), + symbol_short!("transfer"), + (&user1, &pool.address, 1_000_000_000_000_i128).into_val(&env) + )), + sub_invocations: std::vec![], + }, + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + token2.address.clone(), + symbol_short!("transfer"), + (&user1, &pool.address, 1_000_000_000_000_i128).into_val(&env) + )), + sub_invocations: std::vec![], + }, + ], + } + ),] + ); + + assert_eq!(token_share.balance(&user1), 1999999999000); + assert_eq!(token_share.balance(&pool.address), 0); + assert_eq!(token1.balance(&user1), 9000000000000); + assert_eq!(token1.balance(&pool.address), 1000000000000); + assert_eq!(token2.balance(&user1), 9000000000000); + assert_eq!(token2.balance(&pool.address), 1000000000000); + + let result = pool.query_pool_info(); + assert_eq!( + result, + PoolResponse { + asset_a: Asset { + address: token1.address, + amount: 1000000000000i128 + }, + asset_b: Asset { + address: token2.address, + amount: 1000000000000i128 + }, + asset_lp_share: Asset { + address: share_token_address, + amount: 1999999999000i128 + }, + stake_address: pool.query_stake_contract_address(), + } + ); + + assert_eq!(pool.query_total_issued_lp(), 1999999999000); +} + #[test] fn withdraw_liquidity() { let env = Env::default(); diff --git a/contracts/pool_stable/src/tests/swap.rs b/contracts/pool_stable/src/tests/swap.rs index d9cb00b93..61b89c2a1 100644 --- a/contracts/pool_stable/src/tests/swap.rs +++ b/contracts/pool_stable/src/tests/swap.rs @@ -151,6 +151,366 @@ fn simple_swap() { assert_eq!(token2.balance(&user1), 1001 - 1000); // user1 sold 1k of token B on second swap } +#[test] +fn simple_swap_millions_liquidity_swapping_half_milion_no_fee() { + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let factory = Address::generate(&env); + + let mut token1 = deploy_token_contract(&env, &admin); + let mut token2 = deploy_token_contract(&env, &admin); + + if token2.address < token1.address { + std::mem::swap(&mut token1, &mut token2); + } + let user1 = Address::generate(&env); + let swap_fees = 0i64; + let pool = deploy_stable_liquidity_pool_contract( + &env, + None, + (&token1.address, &token2.address), + swap_fees, + None, + None, + None, + manager, + factory, + None, + ); + + // minting 100 million tokens to user1 + token1.mint(&user1, &1_000_000_000_000_000); + token2.mint(&user1, &1_000_000_000_000_000); + // providing 10 million tokens as liquidity from both token1 and token2 + pool.provide_liquidity( + &user1, + &100_000_000_000_000, + &100_000_000_000_000, + &None, + &None::, + &None::, + ); + // at this point, the pool holds: + // token1: 100_000_000_000_000 + // token2: 100_000_000_000_000 + // ttal LP shares issued: 200_000_000_000_000 + + // selling 500_000 tokens with 10% max spread allowed + let spread = 1_000i64; // 10% maximum spread allowed + pool.swap( + &user1, + &token1.address, + &500_000, + &None, + &Some(spread), + &None::, + &Some(150), + ); + // after the swap: + // token1 in the pool increases by 500_000: 100_000_000_000_000 + 500_000 = 100_000_000_500_000 + // token2 in the pool decreases by ~500_000 (depending on swap calculation): 100_000_000_000_000 - 500_000 = 99_999_999_500_000 + // total LP shares remain unchanged at 199_999_999_999_000 (no liquidity added/removed) + + let share_token_address = pool.query_share_token_address(); + let result = pool.query_pool_info(); + assert_eq!( + result, + PoolResponse { + asset_a: Asset { + address: token1.address.clone(), + amount: 100000000500000i128, + }, + asset_b: Asset { + address: token2.address.clone(), + amount: 99999999500000i128, + }, + asset_lp_share: Asset { + address: share_token_address.clone(), + amount: 199999999999000i128, + }, + stake_address: pool.query_stake_contract_address(), + } + ); + + // user's token balances after the first swap: + // token1 decreases by 500_000: 1_000_000_000_000_000 - 500_000 = 899_999_999_500_000 + // token2 increases by ~500_000: 1_000_000_000_000_000 + 500_000 = 900_000_000_500_000 + assert_eq!(token1.balance(&user1), 899999999500000); + assert_eq!(token2.balance(&user1), 900000000500000); + + // this time 100_000 tokens + let output_amount = pool.swap( + &user1, + &token2.address, + &100_000, + &None, + &Some(spread), + &None::, + &None, + ); + + // after the second swap: + // token1 in the pool decreases by ~100_000: 100_000_000_500_000 - 100_000 = 100_000_000_400_000 + // token2 in the pool increases by 100_000: 99_999_999_500_000 + 100_000 = 99_999_999_600_000 + // total LP shares remain unchanged at 199_999_999_999_000 (no liquidity added/removed) + let result = pool.query_pool_info(); + assert_eq!( + result, + PoolResponse { + asset_a: Asset { + address: token1.address.clone(), + amount: 100000000400000, + }, + asset_b: Asset { + address: token2.address.clone(), + amount: 99999999600000, + }, + asset_lp_share: Asset { + address: share_token_address, + amount: 199999999999000 + }, + stake_address: pool.query_stake_contract_address(), + } + ); + + // user's token balances after the second swap: + // token1 increases by ~100_000: 899_999_999_500_000 + 100_000 = 899_999_999_600_000 + // token2 decreases by 100_000: 900_000_000_500_000 - 100_000 = 900_000_000_400_000 + assert_eq!(output_amount, 100_000); + assert_eq!(token1.balance(&user1), 899999999600000); + assert_eq!(token2.balance(&user1), 900000000400000); +} + +#[test] +fn simple_swap_ten_thousand_tokens() { + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let factory = Address::generate(&env); + + let mut token1 = deploy_token_contract(&env, &admin); + let mut token2 = deploy_token_contract(&env, &admin); + + if token2.address < token1.address { + std::mem::swap(&mut token1, &mut token2); + } + let user1 = Address::generate(&env); + let swap_fees = 0i64; + let pool = deploy_stable_liquidity_pool_contract( + &env, + None, + (&token1.address, &token2.address), + swap_fees, + None, + None, + None, + manager, + factory, + None, + ); + + // minting 100 million tokens to user1 + token1.mint(&user1, &1_000_000_000_000_000); + token2.mint(&user1, &1_000_000_000_000_000); + + // providing 10 million tokens as liquidity + pool.provide_liquidity( + &user1, + &100_000_000_000_000, + &100_000_000_000_000, + &None, + &None::, + &None::, + ); + + // selling 10,000 tokens with 5% max spread allowed + let spread = 500i64; + pool.swap( + &user1, + &token1.address, + &1_000_000_000, // 10_000 tokens with 7 decimal precision + &None, + &Some(spread), + &None::, + &Some(150), + ); + + let share_token_address = pool.query_share_token_address(); + let result = pool.query_pool_info(); + + assert_eq!( + result, + PoolResponse { + asset_a: Asset { + address: token1.address.clone(), + amount: 100_001_000_000_000i128, + }, + asset_b: Asset { + address: token2.address.clone(), + amount: 99_999_000_001_428_i128, + }, + asset_lp_share: Asset { + address: share_token_address.clone(), + amount: 199_999_999_999_000i128, + }, + stake_address: pool.query_stake_contract_address(), + } + ); + + assert_eq!(token1.balance(&user1), 899_999_000_000_000); + assert_eq!(token2.balance(&user1), 900_000_999_998_572); +} + +#[test] +fn simple_swap_millions_liquidity_swapping_half_milion_high_fee() { + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().budget().reset_unlimited(); + + let admin = Address::generate(&env); + let manager = Address::generate(&env); + let factory = Address::generate(&env); + + let mut token1 = deploy_token_contract(&env, &admin); + let mut token2 = deploy_token_contract(&env, &admin); + + if token2.address < token1.address { + std::mem::swap(&mut token1, &mut token2); + } + let user1 = Address::generate(&env); + + // we set a 10% swap fee (1000 basis points) + let swap_fees = 1_000i64; // 10% + let pool = deploy_stable_liquidity_pool_contract( + &env, + None, + (&token1.address, &token2.address), + swap_fees, + None, + None, + None, + manager, + factory, + None, + ); + + // minting 1_000_000_000_000_000 "units" + // equals 100_000_000 tokens + token1.mint(&user1, &1_000_000_000_000_000); + token2.mint(&user1, &1_000_000_000_000_000); + + // providing 100_000_000_000_000 "units" of liquidity + // equals 10_000_000 tokens + pool.provide_liquidity( + &user1, + &100_000_000_000_000, + &100_000_000_000_000, + &None, + &None::, + &None::, + ); + + // at this point, the pool holds 100_000_000_000_000 "units" + // equals 10_000_000 tokens + // and total LP shares = 200_000_000_000_000 "units" => 20_000_000 LP shares. + // + // user1 is left with (100-000_000 - 10_000_000) = 90_000_000 tokens (i.e. 900_000_000_000_000 "units"). + + // user sells 10_000_000_000 "units" of token1 + // equals 1_000 tokens, with a 10% max spread allowed. + // Because the pool charges a 10% fee, user gets ~90% of the ideal return in token2. + let spread = 1_000i64; + pool.swap( + &user1, + &token1.address, + &10_000_000_000, + &None, + &Some(spread), + &None::, + &None, + ); + // after this swap: + // token1 in the pool increases by 1_000 tokens (10_000_000,000 "units") + // token2 in the pool decreases by slightly less than 1_000 tokens + // total LP shares remain the same (no liquidity added/removed) + + let share_token_address = pool.query_share_token_address(); + let result = pool.query_pool_info(); + assert_eq!( + result, + PoolResponse { + asset_a: Asset { + address: token1.address.clone(), + amount: 100_010_000_000_000_i128, + }, + asset_b: Asset { + address: token2.address.clone(), + amount: 99_990_000_142_855_i128, + }, + asset_lp_share: Asset { + address: share_token_address.clone(), + amount: 199_999_999_999_000_i128, + }, + stake_address: pool.query_stake_contract_address(), + } + ); + + // user's balances after the first swap: + // token1 decreases by ~1_000 tokens. + // token2 increases by ~900 tokens net (10% fee). + assert_eq!(token1.balance(&user1), 899_990_000_000_000); + assert_eq!(token2.balance(&user1), 900_008_999_871_431); + + // Now user sells 1_000_000_000 "units" of token2 => 100 tokens, + let output_amount = pool.swap( + &user1, + &token2.address, + &1_000_000_000, + &None, + &Some(spread), + &None::, + &None, + ); + + // after this second swap: + // token1 in the pool decreases by ~90 tokens (10% fee) + // token2 in the pool increases by ~100 tokens + let result = pool.query_pool_info(); + assert_eq!( + result, + PoolResponse { + asset_a: Asset { + address: token1.address.clone(), + amount: 100_009_000_115_713, + }, + asset_b: Asset { + address: token2.address.clone(), + amount: 99_991_000_142_855, + }, + asset_lp_share: Asset { + address: share_token_address, + amount: 199_999_999_999_000 + }, + stake_address: pool.query_stake_contract_address(), + } + ); + + assert_eq!(output_amount, 899_895_859); + + // final balances after the second swap: + // token1: 899_990_000_000_000 "units" + ~90 tokens in "units" + // token2: 900_008_999_871_431 "units" - 100 tokens in "units" + assert_eq!(token1.balance(&user1), 899_990_899_895_859); + assert_eq!(token2.balance(&user1), 900_007_999_871_431); +} + #[test] fn swap_with_high_fee() { let env = Env::default();