From a6257fab914ebf5dcca1d3258ea2f99f90ec0c12 Mon Sep 17 00:00:00 2001 From: mrq Date: Fri, 3 Apr 2026 18:14:18 +0200 Subject: [PATCH 1/4] curve parity tests --- integration-tests/src/lib.rs | 1 + .../src/stableswap_curve_comparison.rs | 970 ++++++++++++++++++ .../vyper/CurveStableSwapMath.bin | 1 + .../vyper/CurveStableSwapMath.vy | 259 +++++ 4 files changed, 1231 insertions(+) create mode 100644 integration-tests/src/stableswap_curve_comparison.rs create mode 100644 scripts/test-contracts/vyper/CurveStableSwapMath.bin create mode 100644 scripts/test-contracts/vyper/CurveStableSwapMath.vy diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 786ba36adb..ba56e2c682 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -41,6 +41,7 @@ mod polkadot_test_net; mod referrals; mod router; mod stableswap; +mod stableswap_curve_comparison; mod staking; mod transact_call_filter; mod utility; diff --git a/integration-tests/src/stableswap_curve_comparison.rs b/integration-tests/src/stableswap_curve_comparison.rs new file mode 100644 index 0000000000..b0f139047a --- /dev/null +++ b/integration-tests/src/stableswap_curve_comparison.rs @@ -0,0 +1,970 @@ +#![cfg(test)] + +//! Comparison tests between Hydration's stableswap math and Curve's original StableSwap3Pool math. +//! +//! Deploys a Vyper contract (compiled from Curve's math) on EVM via Frontier, then calls both +//! implementations with identical inputs and compares outputs. +//! +//! The Vyper source: scripts/test-contracts/vyper/CurveStableSwapMath.vy +//! Compiled with: vyper 0.4.3 + +use crate::polkadot_test_net::{Hydra, TestNet}; +use crate::utils::contracts::deploy_contract_code; +use ethabi::{encode, short_signature, ParamType, Token}; +use fp_evm::ExitReason::Succeed; +use fp_evm::ExitSucceed::Returned; +use hydra_dx_math::stableswap::types::AssetReserve; +use hydra_dx_math::stableswap::*; +use hydradx_runtime::{evm::Executor, EVMAccounts, Runtime}; +use hydradx_traits::evm::{CallContext, InspectEvmAccounts, EVM}; +use primitives::{AccountId, EvmAddress}; +use sp_core::U256; +use sp_runtime::Permill; +use xcm_emulator::{Network, TestExt}; + +// Vyper compiled bytecode of CurveStableSwapMath.vy (vyper 0.4.3) +const CURVE_MATH_BYTECODE: &str = include_str!("../../scripts/test-contracts/vyper/CurveStableSwapMath.bin"); + +// Hydration math iteration constants +const D_ITERATIONS: u8 = 64; +const Y_ITERATIONS: u8 = 128; + +// Tolerance bounds (in 18-decimal space, i.e. wei) +// Observed maximums: D=2, swap=4, shares=1, withdraw=3, fees=1 +const MAX_D_TOLERANCE: u128 = 2; +const _MAX_Y_TOLERANCE: u128 = 4; +const MAX_SWAP_TOLERANCE: u128 = 4; +const MAX_SHARE_TOLERANCE: u128 = 2; +// For fee comparisons: max relative tolerance in basis points (0.01%) +const MAX_FEE_RELATIVE_TOLERANCE_BPS: u128 = 1; + +// Curve's fee denominator (10^10) +const _CURVE_FEE_DENOMINATOR: u128 = 10_000_000_000; + +// --- Contract deployment --- + +fn deployer() -> EvmAddress { + EVMAccounts::evm_address(&Into::::into(crate::polkadot_test_net::ALICE)) +} + +fn deploy_curve_math() -> EvmAddress { + let bytecode_hex = CURVE_MATH_BYTECODE.trim(); + let code = hex::decode(&bytecode_hex[2..]).expect("failed to decode curve math bytecode"); + deploy_contract_code(code, deployer()) +} + +// --- ABI encoding helpers --- + +fn u128_to_token(v: u128) -> Token { + Token::Uint(U256::from(v)) +} + +fn u128_array_to_token(arr: &[u128]) -> Token { + Token::Array(arr.iter().map(|v| u128_to_token(*v)).collect()) +} + +fn encode_get_d(xp: &[u128], amp: u128) -> Vec { + let sig = short_signature( + "get_D", + &[ParamType::Array(Box::new(ParamType::Uint(256))), ParamType::Uint(256)], + ); + let tokens = vec![u128_array_to_token(xp), u128_to_token(amp)]; + let mut data = sig.to_vec(); + data.extend(encode(&tokens)); + data +} + +#[allow(dead_code)] +fn encode_get_y(i: usize, j: usize, x: u128, xp: &[u128], amp: u128) -> Vec { + let sig = short_signature( + "get_y", + &[ + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Uint(256), + ], + ); + let tokens = vec![ + u128_to_token(i as u128), + u128_to_token(j as u128), + u128_to_token(x), + u128_array_to_token(xp), + u128_to_token(amp), + ]; + let mut data = sig.to_vec(); + data.extend(encode(&tokens)); + data +} + +fn encode_get_dy(i: usize, j: usize, dx: u128, balances: &[u128], amp: u128, fee: u128) -> Vec { + let sig = short_signature( + "get_dy", + &[ + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Uint(256), + ParamType::Uint(256), + ], + ); + let tokens = vec![ + u128_to_token(i as u128), + u128_to_token(j as u128), + u128_to_token(dx), + u128_array_to_token(balances), + u128_to_token(amp), + u128_to_token(fee), + ]; + let mut data = sig.to_vec(); + data.extend(encode(&tokens)); + data +} + +fn encode_calc_token_amount( + old_balances: &[u128], + new_balances: &[u128], + amp: u128, + token_supply: u128, + fee: u128, +) -> Vec { + let sig = short_signature( + "calc_token_amount", + &[ + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ], + ); + let tokens = vec![ + u128_array_to_token(old_balances), + u128_array_to_token(new_balances), + u128_to_token(amp), + u128_to_token(token_supply), + u128_to_token(fee), + ]; + let mut data = sig.to_vec(); + data.extend(encode(&tokens)); + data +} + +fn encode_calc_withdraw_one_coin( + balances: &[u128], + token_amount: u128, + i: usize, + total_supply: u128, + amp: u128, + fee: u128, +) -> Vec { + let sig = short_signature( + "calc_withdraw_one_coin", + &[ + ParamType::Array(Box::new(ParamType::Uint(256))), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ParamType::Uint(256), + ], + ); + let tokens = vec![ + u128_array_to_token(balances), + u128_to_token(token_amount), + u128_to_token(i as u128), + u128_to_token(total_supply), + u128_to_token(amp), + u128_to_token(fee), + ]; + let mut data = sig.to_vec(); + data.extend(encode(&tokens)); + data +} + +// --- Contract call helpers --- + +fn call_view(contract: EvmAddress, data: Vec) -> Vec { + let context = CallContext::new_view(contract); + let result = Executor::::view(context, data, 5_000_000); + assert_eq!( + result.exit_reason, + Succeed(Returned), + "EVM call failed: {:?} data: {}", + result.exit_reason, + hex::encode(&result.value) + ); + result.value +} + +fn decode_u256(data: &[u8]) -> u128 { + assert!(data.len() >= 32, "response too short: {} bytes", data.len()); + U256::from_big_endian(&data[..32]).as_u128() +} + +fn decode_two_u256(data: &[u8]) -> (u128, u128) { + assert!(data.len() >= 64, "response too short: {} bytes", data.len()); + let a = U256::from_big_endian(&data[..32]).as_u128(); + let b = U256::from_big_endian(&data[32..64]).as_u128(); + (a, b) +} + +// --- Curve contract wrappers --- + +fn curve_get_d(contract: EvmAddress, xp: &[u128], amp: u128) -> u128 { + let data = encode_get_d(xp, amp); + decode_u256(&call_view(contract, data)) +} + +#[allow(dead_code)] +fn curve_get_y(contract: EvmAddress, xp: &[u128], i: usize, j: usize, x: u128, amp: u128) -> u128 { + let data = encode_get_y(i, j, x, xp, amp); + decode_u256(&call_view(contract, data)) +} + +fn curve_get_dy(contract: EvmAddress, balances: &[u128], i: usize, j: usize, dx: u128, amp: u128, fee: u128) -> u128 { + let data = encode_get_dy(i, j, dx, balances, amp, fee); + decode_u256(&call_view(contract, data)) +} + +fn curve_calc_token_amount( + contract: EvmAddress, + old_balances: &[u128], + new_balances: &[u128], + amp: u128, + token_supply: u128, + fee: u128, +) -> u128 { + let data = encode_calc_token_amount(old_balances, new_balances, amp, token_supply, fee); + decode_u256(&call_view(contract, data)) +} + +fn curve_calc_withdraw_one_coin( + contract: EvmAddress, + balances: &[u128], + token_amount: u128, + i: usize, + total_supply: u128, + amp: u128, + fee: u128, +) -> (u128, u128) { + let data = encode_calc_withdraw_one_coin(balances, token_amount, i, total_supply, amp, fee); + decode_two_u256(&call_view(contract, data)) +} + +// --- Hydration math wrappers --- + +fn hydra_get_d(xp: &[u128], amp: u128) -> u128 { + let reserves: Vec = xp.iter().map(|v| AssetReserve::new(*v, 18)).collect(); + let pegs: Vec<(u128, u128)> = vec![(1, 1); xp.len()]; + calculate_d::(&reserves, amp, &pegs).expect("hydra calculate_d failed") +} + +fn hydra_get_dy(balances: &[u128], i: usize, j: usize, dx: u128, amp: u128, fee: Permill) -> u128 { + let reserves: Vec = balances.iter().map(|v| AssetReserve::new(*v, 18)).collect(); + let pegs: Vec<(u128, u128)> = vec![(1, 1); balances.len()]; + if fee == Permill::zero() { + calculate_out_given_in::(&reserves, i, j, dx, amp, &pegs) + .expect("hydra calculate_out_given_in failed") + } else { + let (amount, _fee) = + calculate_out_given_in_with_fee::(&reserves, i, j, dx, amp, fee, &pegs) + .expect("hydra calculate_out_given_in_with_fee failed"); + amount + } +} + +fn hydra_calc_shares( + old_balances: &[u128], + new_balances: &[u128], + amp: u128, + share_issuance: u128, + fee: Permill, +) -> u128 { + let initial: Vec = old_balances.iter().map(|v| AssetReserve::new(*v, 18)).collect(); + let updated: Vec = new_balances.iter().map(|v| AssetReserve::new(*v, 18)).collect(); + let pegs: Vec<(u128, u128)> = vec![(1, 1); old_balances.len()]; + let (shares, _fees) = + calculate_shares::(&initial, &updated, amp, share_issuance, fee, &pegs) + .expect("hydra calculate_shares failed"); + shares +} + +fn hydra_calc_withdraw_one_asset( + balances: &[u128], + shares: u128, + i: usize, + share_issuance: u128, + amp: u128, + fee: Permill, +) -> (u128, u128) { + let reserves: Vec = balances.iter().map(|v| AssetReserve::new(*v, 18)).collect(); + let pegs: Vec<(u128, u128)> = vec![(1, 1); balances.len()]; + calculate_withdraw_one_asset::(&reserves, shares, i, share_issuance, amp, fee, &pegs) + .expect("hydra calculate_withdraw_one_asset failed") +} + +// --- Permill <-> Curve fee conversion --- + +/// Convert Permill to Curve's FEE_DENOMINATOR (10^10) space. +/// Permill is parts per million (10^6), so multiply by 10^4. +fn permill_to_curve_fee(fee: Permill) -> u128 { + let parts: u32 = fee.deconstruct(); + (parts as u128) * 10_000 +} + +// --- Assertion helpers --- + +fn assert_parity(label: &str, hydra: u128, curve: u128, max_tolerance: u128, expect_hydra_gte: bool) { + let diff = hydra.abs_diff(curve); + assert!( + diff <= max_tolerance, + "{}: diff {} exceeds tolerance {} (hydra={}, curve={})", + label, + diff, + max_tolerance, + hydra, + curve + ); + if expect_hydra_gte { + assert!( + hydra >= curve, + "{}: expected hydra ({}) >= curve ({})", + label, + hydra, + curve + ); + } +} + +fn assert_parity_with_fee(label: &str, hydra: u128, curve: u128, max_abs_tolerance: u128) { + let diff = hydra.abs_diff(curve); + // Use max of absolute tolerance and relative tolerance + let relative_tolerance = curve.max(hydra) / 10000 * MAX_FEE_RELATIVE_TOLERANCE_BPS; // 0.01% + let tolerance = max_abs_tolerance.max(relative_tolerance); + assert!( + diff <= tolerance, + "{}: diff {} exceeds tolerance {} (abs={}, rel={}) (hydra={}, curve={})", + label, + diff, + tolerance, + max_abs_tolerance, + relative_tolerance, + hydra, + curve + ); +} + +// ============================================================================= +// D INVARIANT COMPARISON TESTS +// ============================================================================= + +#[test] +fn curve_comparison_d_balanced_2pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 100u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D balanced 2-pool amp=100", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_balanced_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![ + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + ]; + let amp = 2000u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D balanced 3-pool amp=2000", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_imbalanced_2pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000_000_000u128, 500_000_000_000_000_000u128]; + let amp = 100u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D imbalanced 2-pool", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_imbalanced_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![ + 1_000_000_000_000_000_000u128, + 2_000_000_000_000_000_000u128, + 500_000_000_000_000_000u128, + ]; + let amp = 500u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D imbalanced 3-pool", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_high_amp() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 10_000u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D high amp", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_low_amp() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 1u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D low amp", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_large_reserves() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![ + 1_000_000_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000_000_000u128, + ]; + let amp = 100u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D large reserves", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_small_reserves() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000u128, 1_000_000_000_000u128]; + let amp = 100u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D small reserves", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_5pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000_000_000u128; 5]; + let amp = 100u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D 5-pool", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +#[test] +fn curve_comparison_d_extreme_imbalance() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let xp = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000u128]; + let amp = 100u128; + + let curve_d = curve_get_d(contract, &xp, amp); + let hydra_d = hydra_get_d(&xp, amp); + + assert_parity("D extreme imbalance", hydra_d, curve_d, MAX_D_TOLERANCE, true); + }); +} + +// ============================================================================= +// SWAP OUTPUT COMPARISON TESTS (no fee) +// ============================================================================= + +fn run_swap_comparison(label: &str, contract: EvmAddress, balances: &[u128], amp: u128, dx_fraction: u128) { + let dx = balances[0] / dx_fraction; + let i = 0usize; + let j = 1usize; + + // Curve: get_dy with fee=0 + let curve_out = curve_get_dy(contract, balances, i, j, dx, amp, 0); + + // Hydration: calculate_out_given_in (no fee) + let hydra_out = hydra_get_dy(balances, i, j, dx, amp, Permill::zero()); + + // Hydration should give slightly less due to +2 bias and -1 rounding + assert_parity( + &format!("{} swap {}% no fee", label, 100 / dx_fraction), + curve_out, + hydra_out, + MAX_SWAP_TOLERANCE, + true, // curve_out >= hydra_out + ); +} + +#[test] +fn curve_comparison_swap_balanced_2pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 100u128; + + run_swap_comparison("balanced 2-pool", contract, &balances, amp, 100); // 1% + run_swap_comparison("balanced 2-pool", contract, &balances, amp, 10); // 10% + run_swap_comparison("balanced 2-pool", contract, &balances, amp, 10000); // 0.01% + }); +} + +#[test] +fn curve_comparison_swap_imbalanced_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![ + 1_000_000_000_000_000_000u128, + 2_000_000_000_000_000_000u128, + 500_000_000_000_000_000u128, + ]; + let amp = 500u128; + + run_swap_comparison("imbalanced 3-pool", contract, &balances, amp, 100); + run_swap_comparison("imbalanced 3-pool", contract, &balances, amp, 10); + run_swap_comparison("imbalanced 3-pool", contract, &balances, amp, 10000); + }); +} + +#[test] +fn curve_comparison_swap_5pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![1_000_000_000_000_000_000u128; 5]; + let amp = 100u128; + + run_swap_comparison("5-pool", contract, &balances, amp, 100); + run_swap_comparison("5-pool", contract, &balances, amp, 10); + }); +} + +// ============================================================================= +// SWAP WITH FEE COMPARISON TESTS +// ============================================================================= + +fn run_swap_with_fee_comparison( + label: &str, + contract: EvmAddress, + balances: &[u128], + amp: u128, + dx_fraction: u128, + fee_permill: Permill, +) { + let dx = balances[0] / dx_fraction; + let i = 0usize; + let j = 1usize; + let curve_fee = permill_to_curve_fee(fee_permill); + + let curve_out = curve_get_dy(contract, balances, i, j, dx, amp, curve_fee); + let hydra_out = hydra_get_dy(balances, i, j, dx, amp, fee_permill); + + assert_parity_with_fee( + &format!("{} swap {}% fee={:?}", label, 100 / dx_fraction, fee_permill), + hydra_out, + curve_out, + MAX_SWAP_TOLERANCE, + ); +} + +#[test] +fn curve_comparison_swap_with_fee_004pct() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 100u128; + let fee = Permill::from_parts(400); // 0.04% + + run_swap_with_fee_comparison("2-pool", contract, &balances, amp, 100, fee); + run_swap_with_fee_comparison("2-pool", contract, &balances, amp, 10, fee); + }); +} + +#[test] +fn curve_comparison_swap_with_fee_03pct() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![ + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + ]; + let amp = 2000u128; + let fee = Permill::from_parts(3000); // 0.3% + + run_swap_with_fee_comparison("3-pool", contract, &balances, amp, 100, fee); + run_swap_with_fee_comparison("3-pool", contract, &balances, amp, 10, fee); + }); +} + +#[test] +fn curve_comparison_swap_with_fee_1pct() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 100u128; + let fee = Permill::from_parts(10000); // 1% + + run_swap_with_fee_comparison("2-pool 1%", contract, &balances, amp, 100, fee); + run_swap_with_fee_comparison("2-pool 1%", contract, &balances, amp, 10, fee); + }); +} + +// ============================================================================= +// ADD LIQUIDITY / SHARE CALCULATION COMPARISON TESTS +// ============================================================================= + +#[test] +fn curve_comparison_shares_balanced_deposit_no_fee() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let old = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let new = vec![1_100_000_000_000_000_000u128, 1_100_000_000_000_000_000u128]; // +10% balanced + let amp = 100u128; + let supply = 2_000_000_000_000_000_000u128; // D of balanced pool + + let curve_shares = curve_calc_token_amount(contract, &old, &new, amp, supply, 0); + let hydra_shares = hydra_calc_shares(&old, &new, amp, supply, Permill::zero()); + + assert_parity( + "shares balanced deposit no fee", + curve_shares, + hydra_shares, + MAX_SHARE_TOLERANCE, + false, + ); + }); +} + +#[test] +fn curve_comparison_shares_single_sided_no_fee() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let old = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let new = vec![1_100_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; // +10% single-sided + let amp = 100u128; + let supply = 2_000_000_000_000_000_000u128; + + let curve_shares = curve_calc_token_amount(contract, &old, &new, amp, supply, 0); + let hydra_shares = hydra_calc_shares(&old, &new, amp, supply, Permill::zero()); + + assert_parity( + "shares single-sided no fee", + curve_shares, + hydra_shares, + MAX_SHARE_TOLERANCE, + false, + ); + }); +} + +#[test] +fn curve_comparison_shares_3pool_imbalanced_no_fee() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let old = vec![ + 1_000_000_000_000_000_000u128, + 2_000_000_000_000_000_000u128, + 500_000_000_000_000_000u128, + ]; + let new = vec![ + 1_200_000_000_000_000_000u128, + 2_000_000_000_000_000_000u128, + 600_000_000_000_000_000u128, + ]; + let amp = 500u128; + let supply = 3_400_000_000_000_000_000u128; // approximate D + + let curve_shares = curve_calc_token_amount(contract, &old, &new, amp, supply, 0); + let hydra_shares = hydra_calc_shares(&old, &new, amp, supply, Permill::zero()); + + assert_parity( + "shares 3-pool imbalanced no fee", + curve_shares, + hydra_shares, + MAX_SHARE_TOLERANCE, + false, + ); + }); +} + +// --- Shares with fees --- + +fn run_shares_with_fee_comparison( + label: &str, + contract: EvmAddress, + old: &[u128], + new: &[u128], + amp: u128, + supply: u128, + fee: Permill, +) { + let curve_fee = permill_to_curve_fee(fee); + let curve_shares = curve_calc_token_amount(contract, old, new, amp, supply, curve_fee); + let hydra_shares = hydra_calc_shares(old, new, amp, supply, fee); + + assert_parity_with_fee( + &format!("{} fee={:?}", label, fee), + hydra_shares, + curve_shares, + MAX_SHARE_TOLERANCE, + ); +} + +#[test] +fn curve_comparison_shares_with_fee_single_sided() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let old = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let new = vec![1_100_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let amp = 100u128; + let supply = 2_000_000_000_000_000_000u128; + + run_shares_with_fee_comparison("single-sided 0.04%", contract, &old, &new, amp, supply, Permill::from_parts(400)); + run_shares_with_fee_comparison("single-sided 0.3%", contract, &old, &new, amp, supply, Permill::from_parts(3000)); + run_shares_with_fee_comparison("single-sided 1%", contract, &old, &new, amp, supply, Permill::from_parts(10000)); + }); +} + +#[test] +fn curve_comparison_shares_with_fee_balanced_deposit() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let old = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let new = vec![1_100_000_000_000_000_000u128, 1_100_000_000_000_000_000u128]; + let amp = 100u128; + let supply = 2_000_000_000_000_000_000u128; + + // Balanced deposit should have near-zero fee impact + run_shares_with_fee_comparison("balanced 0.3%", contract, &old, &new, amp, supply, Permill::from_parts(3000)); + run_shares_with_fee_comparison("balanced 1%", contract, &old, &new, amp, supply, Permill::from_parts(10000)); + }); +} + +#[test] +fn curve_comparison_shares_with_fee_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let old = vec![ + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + ]; + // Single-sided deposit into 3-pool + let new = vec![ + 1_200_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + ]; + let amp = 2000u128; + let supply = 3_000_000_000_000_000_000u128; + + run_shares_with_fee_comparison("3-pool single-sided 0.04%", contract, &old, &new, amp, supply, Permill::from_parts(400)); + run_shares_with_fee_comparison("3-pool single-sided 0.3%", contract, &old, &new, amp, supply, Permill::from_parts(3000)); + run_shares_with_fee_comparison("3-pool single-sided 1%", contract, &old, &new, amp, supply, Permill::from_parts(10000)); + }); +} + +// ============================================================================= +// SINGLE-ASSET WITHDRAWAL COMPARISON TESTS +// ============================================================================= + +#[test] +fn curve_comparison_withdraw_no_fee() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let total_supply = 2_000_000_000_000_000_000u128; + let withdraw_shares = 100_000_000_000_000_000u128; // 5% of supply + let amp = 100u128; + + let (curve_dy, _) = curve_calc_withdraw_one_coin(contract, &balances, withdraw_shares, 0, total_supply, amp, 0); + let (hydra_dy, _) = hydra_calc_withdraw_one_asset(&balances, withdraw_shares, 0, total_supply, amp, Permill::zero()); + + assert_parity( + "withdraw no fee amount", + curve_dy, + hydra_dy, + MAX_SWAP_TOLERANCE, + false, + ); + }); +} + +#[test] +fn curve_comparison_withdraw_imbalanced_no_fee() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![ + 1_000_000_000_000_000_000u128, + 2_000_000_000_000_000_000u128, + 500_000_000_000_000_000u128, + ]; + let total_supply = 3_400_000_000_000_000_000u128; + let withdraw_shares = 100_000_000_000_000_000u128; + let amp = 500u128; + + let (curve_dy, _) = curve_calc_withdraw_one_coin(contract, &balances, withdraw_shares, 0, total_supply, amp, 0); + let (hydra_dy, _) = hydra_calc_withdraw_one_asset(&balances, withdraw_shares, 0, total_supply, amp, Permill::zero()); + + assert_parity( + "withdraw imbalanced 3-pool no fee", + curve_dy, + hydra_dy, + MAX_SWAP_TOLERANCE, + false, + ); + }); +} + +// --- Withdrawal with fees --- + +fn run_withdraw_with_fee_comparison( + label: &str, + contract: EvmAddress, + balances: &[u128], + withdraw_shares: u128, + i: usize, + total_supply: u128, + amp: u128, + fee: Permill, +) { + let curve_fee_val = permill_to_curve_fee(fee); + let (curve_dy, curve_fee_amount) = + curve_calc_withdraw_one_coin(contract, balances, withdraw_shares, i, total_supply, amp, curve_fee_val); + let (hydra_dy, hydra_fee_amount) = + hydra_calc_withdraw_one_asset(balances, withdraw_shares, i, total_supply, amp, fee); + + assert_parity_with_fee( + &format!("{} withdraw amount", label), + hydra_dy, + curve_dy, + MAX_SWAP_TOLERANCE, + ); + assert_parity_with_fee( + &format!("{} withdraw fee", label), + hydra_fee_amount, + curve_fee_amount, + MAX_SWAP_TOLERANCE, + ); +} + +#[test] +fn curve_comparison_withdraw_with_fee_2pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![1_000_000_000_000_000_000u128, 1_000_000_000_000_000_000u128]; + let total_supply = 2_000_000_000_000_000_000u128; + let withdraw_shares = 100_000_000_000_000_000u128; + let amp = 100u128; + + run_withdraw_with_fee_comparison("2-pool 0.04%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(400)); + run_withdraw_with_fee_comparison("2-pool 0.3%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(3000)); + run_withdraw_with_fee_comparison("2-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); + }); +} + +#[test] +fn curve_comparison_withdraw_with_fee_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![ + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + 1_000_000_000_000_000_000u128, + ]; + let total_supply = 3_000_000_000_000_000_000u128; + let withdraw_shares = 150_000_000_000_000_000u128; // 5% + let amp = 2000u128; + + run_withdraw_with_fee_comparison("3-pool 0.04%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(400)); + run_withdraw_with_fee_comparison("3-pool 0.3%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(3000)); + run_withdraw_with_fee_comparison("3-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); + }); +} + +#[test] +fn curve_comparison_withdraw_with_fee_imbalanced_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let contract = deploy_curve_math(); + let balances = vec![ + 1_000_000_000_000_000_000u128, + 2_000_000_000_000_000_000u128, + 500_000_000_000_000_000u128, + ]; + let total_supply = 3_400_000_000_000_000_000u128; + let withdraw_shares = 100_000_000_000_000_000u128; + let amp = 500u128; + + run_withdraw_with_fee_comparison("imbalanced 3-pool 0.3%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(3000)); + run_withdraw_with_fee_comparison("imbalanced 3-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); + }); +} diff --git a/scripts/test-contracts/vyper/CurveStableSwapMath.bin b/scripts/test-contracts/vyper/CurveStableSwapMath.bin new file mode 100644 index 0000000000..b71a019554 --- /dev/null +++ b/scripts/test-contracts/vyper/CurveStableSwapMath.bin @@ -0,0 +1 @@ +0x6110c8610011610000396110c8610000f35f3560e01c60026005820660011b6110be01601e395f51565b63ac87d5e0811861007b576044361034176110ba5760043560040160088135116110ba57803560208160051b0180836102803750505060206102805160208160051b018061028060405e5050602435610160526100766103a0610938565b6103a0f35b635932cea981186109345760c4361034176110ba5760043560040160088135116110ba57803560208160051b0180836102e0375050506102e051610400526104005160443510156110ba575f6104205260a435156101215760a435610400518082028115838383041417156110ba579050905061040051600181038181116110ba5790508060021b818160021c186110ba57905080156110ba5780820490509050610420525b6102e05160208160051b01806102e060405e505060843561016052610147610460610938565b610460516104405261044051602435610440518082028115838383041417156110ba579050905060643580156110ba57808204905090508082038281116110ba5790509050610460526084356040526044356060526102e05160208160051b01806102e060805e5050610460516101a0526101c36104a0610e59565b6104a051610480526044356102e0518110156110ba5760051b6103000151610480518082038281116110ba57905090506104a0525f6104c0525f6008905b806105e052610400516105e051101561035e575f610600526044356105e05118610280576105e0516102e0518110156110ba5760051b6103000151610460518082028115838383041417156110ba57905090506104405180156110ba5780820490509050610480518082038281116110ba5790509050610600526102ea565b6105e0516102e0518110156110ba5760051b61030001516105e0516102e0518110156110ba5760051b6103000151610460518082028115838383041417156110ba57905090506104405180156110ba57808204905090508082038281116110ba5790509050610600525b6104c051600781116110ba576105e0516102e0518110156110ba5760051b610300015161042051610600518082028115838383041417156110ba57905090506402540be400810490508082038281116110ba57905090508160051b6104e00152600181016104c05250600101818118610201575b50506044356104c0518110156110ba5760051b6104e001516084356040526044356060526104c05160208160051b01806104c060805e5050610460516101a0526103a9610600610e59565b610600518082038281116110ba57905090506105e0526105e051600181038181116110ba5790506105e0526104a0516105e0518082038281116110ba57905090506106005260406105e06106205e6040610620f35b6316e557ea81186109345760a4361034176110ba5760643560040160088135116110ba57803560208160051b01808361056037505050602060606004610280376105605160208160051b01806105606102e05e505060843561040052610465610680610b8d565b610680f35b63520835dc81186104d4576084361034176110ba5760443560040160088135116110ba57803560208160051b0180836102e0375050506020604060046040376102e05160208160051b01806102e060805e50506064356101a0526104cf610400610e59565b610400f35b63304956c481186109345760a4361034176110ba5760043560040160088135116110ba57803560208160051b0180836102803750505060243560040160088135116110ba57803560208160051b0180836103a037505050610280516104c0526104c0516103a051186110ba576102805160208160051b018061028060405e505060443561016052610566610500610938565b610500516104e0526103a05160208160051b01806103a060405e505060443561016052610594610520610938565b61052051610500526104e0516105005111156110ba576105005161052052606435156105c45760843515156105c6565b5f5b156107a3576084356104c0518082028115838383041417156110ba57905090506104c051600181038181116110ba5790508060021b818160021c186110ba57905080156110ba5780820490509050610540525f610560525f6008905b80610680526104c051610680511015610772576105005161068051610280518110156110ba5760051b6102a001518082028115838383041417156110ba57905090506104e05180156110ba57808204905090506106a0525f6106c052610680516103a0518110156110ba5760051b6103c001516106a051116106d057610680516103a0518110156110ba5760051b6103c001516106a0518082038281116110ba57905090506106c0526106fe565b6106a051610680516103a0518110156110ba5760051b6103c001518082038281116110ba57905090506106c0525b61056051600781116110ba57610680516103a0518110156110ba5760051b6103c00151610540516106c0518082028115838383041417156110ba57905090506402540be400810490508082038281116110ba57905090508160051b6105800152600181016105605250600101818118610622575b50506105605160208160051b018061056060405e50506044356101605261079a610680610938565b61068051610520525b6064356107b45760206105006107fc565b606435610520516104e0518082038281116110ba57905090508082028115838383041417156110ba57905090506104e05180156110ba57808204905090506105405260206105405bf35b63fc86422781186109345760c4361034176110ba5760643560040160088135116110ba57803560208160051b01808361056037505050600435610560518110156110ba5760051b61058001516044358082018281106110ba5790509050610680526040600461028037610680516102c0526105605160208160051b01806105606102e05e5050608435610400526108966106c0610b8d565b6106c0516106a052602435610560518110156110ba5760051b61058001516106a0518082038281116110ba5790509050600181038181116110ba5790506106c05260a4351561092c5760a4356106c0518082028115838383041417156110ba57905090506402540be400810490506106e0526106c0516106e0518082038281116110ba5790509050610700526020610700610932565b60206106c05bf35b5f5ffd5b604051610180525f6101a0525f604051600881116110ba57801561098957905b8060051b606001516101c0526101a0516101c0518082018281106110ba57905090506101a052600101818118610958575b50506101a05161099c575f815250610b8b565b5f6101c0526101a0516101e05261016051610180518082028115838383041417156110ba5790509050610200525f60ff905b80610220526101e051610240525f604051600881116110ba578015610a4f57905b8060051b6060015161026052610240516101e0518082028115838383041417156110ba579050905061026051610180518082028115838383041417156110ba579050905080156110ba5780820490509050610240526001018181186109ef575b50506101e0516101c052610200516101a0518082028115838383041417156110ba579050905061024051610180518082028115838383041417156110ba57905090508082018281106110ba57905090506101e0518082028115838383041417156110ba579050905061020051600181038181116110ba5790506101e0518082028115838383041417156110ba579050905061018051600181018181106110ba579050610240518082028115838383041417156110ba57905090508082018281106110ba579050905080156110ba57808204905090506101e0526101c0516101e05111610b575760016101c0516101e0518082038281116110ba579050905011610b7657610b81565b60016101e0516101c0518082038281116110ba57905090501115610b81575b6001018181186109ce575b50506101e0518152505b565b6102e051610420526102a05161028051146110ba57610420516102a05110156110ba57610420516102805110156110ba576102e05160208160051b01806102e060405e50506104005161016052610be5610460610938565b610460516104405261044051610460525f6104805261040051610420518082028115838383041417156110ba57905090506104a0525f6104c0525f6008905b806104e052610420516104e0511015610ce957610280516104e05118610c51576102c0516104c052610c7a565b6102a0516104e05114610cde576104e0516102e0518110156110ba5760051b61030001516104c0525b610480516104c0518082018281106110ba57905090506104805261046051610440518082028115838383041417156110ba57905090506104c051610420518082028115838383041417156110ba579050905080156110ba5780820490509050610460525b600101818118610c24575b505061046051610440518082028115838383041417156110ba57905090506104a051610420518082028115838383041417156110ba579050905080156110ba57808204905090506104605261048051610440516104a05180156110ba57808204905090508082018281106110ba57905090506104e0525f6105005261044051610520525f60ff905b8061054052610520516105005261052051610520518082028115838383041417156110ba5790509050610460518082018281106110ba5790509050610520518060011b818160011c186110ba5790506104e0518082018281106110ba5790509050610440518082038281116110ba579050905080156110ba578082049050905061052052610500516105205111610e2457600161050051610520518082038281116110ba579050905011610e4357610e4e565b600161052051610500518082038281116110ba57905090501115610e4e575b600101818118610d71575b505061052051815250565b6080516101c0526101c05160605110156110ba576101a0516101e0525f610200526040516101c0518082028115838383041417156110ba5790509050610220525f610240525f6008905b80610260526101c051610260511015610f4a576060516102605114610f3f57610260516080518110156110ba5760051b60a001516102405261020051610240518082018281106110ba5790509050610200526101e0516101a0518082028115838383041417156110ba5790509050610240516101c0518082028115838383041417156110ba579050905080156110ba57808204905090506101e0525b600101818118610ea3575b50506101e0516101a0518082028115838383041417156110ba5790509050610220516101c0518082028115838383041417156110ba579050905080156110ba57808204905090506101e052610200516101a0516102205180156110ba57808204905090508082018281106110ba5790509050610260525f610280526101a0516102a0525f60ff905b806102c0526102a051610280526102a0516102a0518082028115838383041417156110ba57905090506101e0518082018281106110ba57905090506102a0518060011b818160011c186110ba579050610260518082018281106110ba57905090506101a0518082038281116110ba579050905080156110ba57808204905090506102a052610280516102a05111611085576001610280516102a0518082038281116110ba5790509050116110a4576110af565b60016102a051610280518082038281116110ba579050905011156110af575b600101818118610fd2575b50506102a051815250565b5f80fd093407fe03fe046a00188558200bcfe2b37f9e83fbf30710da9ae9597be841ac1112821c8bc69d0b43fda4e9a31910c8810a00a1657679706572830004030036 diff --git a/scripts/test-contracts/vyper/CurveStableSwapMath.vy b/scripts/test-contracts/vyper/CurveStableSwapMath.vy new file mode 100644 index 0000000000..66164b8da9 --- /dev/null +++ b/scripts/test-contracts/vyper/CurveStableSwapMath.vy @@ -0,0 +1,259 @@ +# @version ^0.4.0 + +# Faithful extraction of Curve StableSwap math from StableSwap3Pool.vy +# Source: https://github.com/curvefi/curve-contract/blob/master/contracts/pools/3pool/StableSwap3Pool.vy +# +# Changes from original: +# - Internal functions made external with @pure decorator +# - Fixed-size arrays (uint256[N_COINS]) replaced with DynArray[uint256, 8] +# - N_COINS derived from array length at runtime +# - No state, no tokens, no admin — pure math only +# +# Compiled with: vyper 0.4.3 +# Command: vyper scripts/test-contracts/vyper/CurveStableSwapMath.vy -f bytecode + +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 + + +@internal +@pure +def _get_D(xp: DynArray[uint256, 8], amp: uint256) -> uint256: + """ + Curve's get_D — Newton's method for the StableSwap invariant D. + Solves: A*n^n*sum(x_i) + D = A*D*n^n + D^(n+1)/(n^n * prod(x_i)) + """ + N_COINS: uint256 = len(xp) + S: uint256 = 0 + for x: uint256 in xp: + S += x + if S == 0: + return 0 + + Dprev: uint256 = 0 + D: uint256 = S + Ann: uint256 = amp * N_COINS + for _i: uint256 in range(255): + D_P: uint256 = D + for x: uint256 in xp: + D_P = D_P * D // (x * N_COINS) + Dprev = D + D = (Ann * S + D_P * N_COINS) * D // ((Ann - 1) * D + (N_COINS + 1) * D_P) + # Convergence check + if D > Dprev: + if D - Dprev <= 1: + break + else: + if Dprev - D <= 1: + break + return D + + +@external +@pure +def get_D(xp: DynArray[uint256, 8], amp: uint256) -> uint256: + return self._get_D(xp, amp) + + +@internal +@pure +def _get_y(i: uint256, j: uint256, x: uint256, xp: DynArray[uint256, 8], amp: uint256) -> uint256: + """ + Curve's get_y — solve for the new balance of coin j. + Given coin i has new balance x, find y_j that satisfies the invariant. + """ + N_COINS: uint256 = len(xp) + assert i != j + assert j < N_COINS + assert i < N_COINS + + D: uint256 = self._get_D(xp, amp) + c: uint256 = D + S_: uint256 = 0 + Ann: uint256 = amp * N_COINS + + _x: uint256 = 0 + for _i: uint256 in range(8): + if _i >= N_COINS: + break + if _i == i: + _x = x + elif _i != j: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D // (_x * N_COINS) + c = c * D // (Ann * N_COINS) + b: uint256 = S_ + D // Ann + y_prev: uint256 = 0 + y: uint256 = D + for _i: uint256 in range(255): + y_prev = y + y = (y * y + c) // (2 * y + b - D) + # Convergence check + if y > y_prev: + if y - y_prev <= 1: + break + else: + if y_prev - y <= 1: + break + return y + + +@external +@pure +def get_y(i: uint256, j: uint256, x: uint256, xp: DynArray[uint256, 8], amp: uint256) -> uint256: + return self._get_y(i, j, x, xp, amp) + + +@internal +@pure +def _get_y_D(amp: uint256, i: uint256, xp: DynArray[uint256, 8], D: uint256) -> uint256: + """ + Curve's get_y_D — solve for balance of coin i at a given D value. + Used by calc_withdraw_one_coin. + """ + N_COINS: uint256 = len(xp) + assert i < N_COINS + + c: uint256 = D + S_: uint256 = 0 + Ann: uint256 = amp * N_COINS + + _x: uint256 = 0 + for _i: uint256 in range(8): + if _i >= N_COINS: + break + if _i != i: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D // (_x * N_COINS) + c = c * D // (Ann * N_COINS) + b: uint256 = S_ + D // Ann + y_prev: uint256 = 0 + y: uint256 = D + for _i: uint256 in range(255): + y_prev = y + y = (y * y + c) // (2 * y + b - D) + if y > y_prev: + if y - y_prev <= 1: + break + else: + if y_prev - y <= 1: + break + return y + + +@external +@pure +def get_y_D(amp: uint256, i: uint256, xp: DynArray[uint256, 8], D: uint256) -> uint256: + return self._get_y_D(amp, i, xp, D) + + +@external +@pure +def calc_token_amount( + old_balances: DynArray[uint256, 8], + new_balances: DynArray[uint256, 8], + amp: uint256, + token_supply: uint256, + fee: uint256, +) -> uint256: + """ + Curve's add_liquidity share calculation. + Returns the number of LP tokens minted for a deposit. + fee is in units of FEE_DENOMINATOR (10^10). Pass 0 to disable fees. + """ + N_COINS: uint256 = len(old_balances) + assert len(new_balances) == N_COINS + + D0: uint256 = self._get_D(old_balances, amp) + D1: uint256 = self._get_D(new_balances, amp) + assert D1 > D0 + + D2: uint256 = D1 + if token_supply > 0 and fee > 0: + _fee: uint256 = fee * N_COINS // (4 * (N_COINS - 1)) + adjusted: DynArray[uint256, 8] = [] + for i: uint256 in range(8): + if i >= N_COINS: + break + ideal_balance: uint256 = D1 * old_balances[i] // D0 + difference: uint256 = 0 + if ideal_balance > new_balances[i]: + difference = ideal_balance - new_balances[i] + else: + difference = new_balances[i] - ideal_balance + adjusted.append(new_balances[i] - (_fee * difference // FEE_DENOMINATOR)) + D2 = self._get_D(adjusted, amp) + + if token_supply == 0: + return D1 + else: + return token_supply * (D2 - D0) // D0 + + +@external +@pure +def calc_withdraw_one_coin( + balances: DynArray[uint256, 8], + token_amount: uint256, + i: uint256, + total_supply: uint256, + amp: uint256, + fee: uint256, +) -> (uint256, uint256): + """ + Curve's _calc_withdraw_one_coin math. + Returns (dy, dy_fee) — amount received and fee amount. + fee is in units of FEE_DENOMINATOR (10^10). Pass 0 to disable fees. + """ + N_COINS: uint256 = len(balances) + assert i < N_COINS + + _fee: uint256 = 0 + if fee > 0: + _fee = fee * N_COINS // (4 * (N_COINS - 1)) + + D0: uint256 = self._get_D(balances, amp) + D1: uint256 = D0 - token_amount * D0 // total_supply + + new_y: uint256 = self._get_y_D(amp, i, balances, D1) + dy_0: uint256 = balances[i] - new_y + + xp_reduced: DynArray[uint256, 8] = [] + for j: uint256 in range(8): + if j >= N_COINS: + break + dx_expected: uint256 = 0 + if j == i: + dx_expected = balances[j] * D1 // D0 - new_y + else: + dx_expected = balances[j] - balances[j] * D1 // D0 + xp_reduced.append(balances[j] - (_fee * dx_expected // FEE_DENOMINATOR)) + + dy: uint256 = xp_reduced[i] - self._get_y_D(amp, i, xp_reduced, D1) + dy = dy - 1 # Withdraw less to account for rounding errors + + dy_fee: uint256 = dy_0 - dy + + return (dy, dy_fee) + + +@external +@pure +def get_dy(i: uint256, j: uint256, dx: uint256, balances: DynArray[uint256, 8], amp: uint256, fee: uint256) -> uint256: + """ + Curve's get_dy — calculate swap output amount. + Returns the amount of coin j received for dx of coin i, after fee. + fee is in units of FEE_DENOMINATOR (10^10). Pass 0 to disable fees. + """ + x: uint256 = balances[i] + dx + y: uint256 = self._get_y(i, j, x, balances, amp) + dy: uint256 = balances[j] - y - 1 + if fee > 0: + _fee: uint256 = fee * dy // FEE_DENOMINATOR + return dy - _fee + return dy From 8c1fb826d3c727aa400a42bfc0c6ad0e90ae206a Mon Sep 17 00:00:00 2001 From: mrq Date: Fri, 3 Apr 2026 18:28:45 +0200 Subject: [PATCH 2/4] add & remove cycle test --- .../src/stableswap_curve_comparison.rs | 408 ++++++++++++++++++ 1 file changed, 408 insertions(+) diff --git a/integration-tests/src/stableswap_curve_comparison.rs b/integration-tests/src/stableswap_curve_comparison.rs index b0f139047a..215aba8b1e 100644 --- a/integration-tests/src/stableswap_curve_comparison.rs +++ b/integration-tests/src/stableswap_curve_comparison.rs @@ -968,3 +968,411 @@ fn curve_comparison_withdraw_with_fee_imbalanced_3pool() { run_withdraw_with_fee_comparison("imbalanced 3-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); }); } + +// ============================================================================= +// BALANCED ADD + PROPORTIONAL REMOVE CYCLE TEST +// ============================================================================= +// Tests whether repeated balanced deposits and proportional withdrawals can +// extract value from the pool through rounding. + +#[test] +fn curve_comparison_balanced_add_remove_cycle_no_value_extraction() { + TestNet::reset(); + Hydra::execute_with(|| { + let amp = 100u128; + let n_assets = 2usize; + let fee = Permill::from_parts(500); // 0.05% + + // Initial pool state + let mut reserves = vec![1_000_000_000_000_000_000u128; n_assets]; + let mut share_issuance = hydra_get_d(&reserves, amp); + + // Attacker starts with these tokens + let deposit_per_asset = 100_000_000_000_000_000u128; // 0.1 token per asset + let mut attacker_balances: Vec = vec![deposit_per_asset; n_assets]; + + let iterations = 100u32; + + for _ in 0..iterations { + // Step 1: Balanced deposit + let new_reserves: Vec = reserves + .iter() + .zip(attacker_balances.iter()) + .map(|(r, a)| r + a) + .collect(); + + let shares_received = hydra_calc_shares(&reserves, &new_reserves, amp, share_issuance, fee); + assert!(shares_received > 0, "should receive shares"); + + // Update pool state after deposit + reserves = new_reserves; + share_issuance += shares_received; + + // Step 2: Proportional withdrawal of all shares received + let mut withdrawn: Vec = Vec::new(); + for i in 0..n_assets { + let amount = calculate_liquidity_out(reserves[i], shares_received, share_issuance) + .expect("liquidity out failed"); + withdrawn.push(amount); + } + + // Update pool state after withdrawal + for i in 0..n_assets { + reserves[i] -= withdrawn[i]; + } + share_issuance -= shares_received; + + // Update attacker balances + attacker_balances = withdrawn; + } + + // Check: attacker should NOT have more than they started with + let initial_total = deposit_per_asset * n_assets as u128; + let final_total: u128 = attacker_balances.iter().sum(); + + eprintln!( + "balanced add+remove cycle ({}x): initial={} final={} diff={} ({})", + iterations, + initial_total, + final_total, + initial_total as i128 - final_total as i128, + if final_total <= initial_total { + "protocol safe" + } else { + "VALUE EXTRACTED" + } + ); + + assert!( + final_total <= initial_total, + "attacker extracted value! initial={} final={} profit={}", + initial_total, + final_total, + final_total - initial_total, + ); + }); +} + +#[test] +fn curve_comparison_balanced_add_remove_cycle_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let amp = 2000u128; + let n_assets = 3usize; + let fee = Permill::from_parts(500); // 0.05% + + let mut reserves = vec![1_000_000_000_000_000_000u128; n_assets]; + let mut share_issuance = hydra_get_d(&reserves, amp); + + let deposit_per_asset = 100_000_000_000_000_000u128; + let mut attacker_balances: Vec = vec![deposit_per_asset; n_assets]; + + let iterations = 100u32; + + for _ in 0..iterations { + let new_reserves: Vec = reserves + .iter() + .zip(attacker_balances.iter()) + .map(|(r, a)| r + a) + .collect(); + + let shares_received = hydra_calc_shares(&reserves, &new_reserves, amp, share_issuance, fee); + assert!(shares_received > 0); + + reserves = new_reserves; + share_issuance += shares_received; + + let mut withdrawn: Vec = Vec::new(); + for i in 0..n_assets { + let amount = calculate_liquidity_out(reserves[i], shares_received, share_issuance) + .expect("liquidity out failed"); + withdrawn.push(amount); + } + + for i in 0..n_assets { + reserves[i] -= withdrawn[i]; + } + share_issuance -= shares_received; + + attacker_balances = withdrawn; + } + + let initial_total = deposit_per_asset * n_assets as u128; + let final_total: u128 = attacker_balances.iter().sum(); + + eprintln!( + "3-pool balanced add+remove cycle ({}x): initial={} final={} diff={} ({})", + iterations, + initial_total, + final_total, + initial_total as i128 - final_total as i128, + if final_total <= initial_total { + "protocol safe" + } else { + "VALUE EXTRACTED" + } + ); + + assert!( + final_total <= initial_total, + "attacker extracted value! initial={} final={} profit={}", + initial_total, + final_total, + final_total - initial_total, + ); + }); +} + +#[test] +fn curve_comparison_balanced_add_remove_cycle_zero_fee() { + TestNet::reset(); + Hydra::execute_with(|| { + let amp = 100u128; + let n_assets = 2usize; + let fee = Permill::zero(); // no fee — worst case for protocol + + let mut reserves = vec![1_000_000_000_000_000_000u128; n_assets]; + let mut share_issuance = hydra_get_d(&reserves, amp); + + let deposit_per_asset = 100_000_000_000_000_000u128; + let mut attacker_balances: Vec = vec![deposit_per_asset; n_assets]; + + let iterations = 1000u32; + + for _ in 0..iterations { + let new_reserves: Vec = reserves + .iter() + .zip(attacker_balances.iter()) + .map(|(r, a)| r + a) + .collect(); + + let shares_received = hydra_calc_shares(&reserves, &new_reserves, amp, share_issuance, fee); + assert!(shares_received > 0); + + reserves = new_reserves; + share_issuance += shares_received; + + let mut withdrawn: Vec = Vec::new(); + for i in 0..n_assets { + let amount = calculate_liquidity_out(reserves[i], shares_received, share_issuance) + .expect("liquidity out failed"); + withdrawn.push(amount); + } + + for i in 0..n_assets { + reserves[i] -= withdrawn[i]; + } + share_issuance -= shares_received; + + attacker_balances = withdrawn; + } + + let initial_total = deposit_per_asset * n_assets as u128; + let final_total: u128 = attacker_balances.iter().sum(); + + eprintln!( + "zero-fee balanced add+remove cycle ({}x): initial={} final={} diff={} ({})", + iterations, + initial_total, + final_total, + initial_total as i128 - final_total as i128, + if final_total <= initial_total { + "protocol safe" + } else { + "VALUE EXTRACTED" + } + ); + + assert!( + final_total <= initial_total, + "attacker extracted value! initial={} final={} profit={}", + initial_total, + final_total, + final_total - initial_total, + ); + }); +} + +// ============================================================================= +// IMBALANCED POOL: BALANCED ADD + PROPORTIONAL REMOVE CYCLE TESTS +// ============================================================================= + +/// Helper: run proportional add + proportional remove cycle on any pool state. +fn run_add_remove_cycle( + label: &str, + initial_reserves: Vec, + amp: u128, + fee: Permill, + deposit_fraction: u128, // deposit = reserve[i] / deposit_fraction + iterations: u32, +) { + let n_assets = initial_reserves.len(); + let mut reserves = initial_reserves; + let mut share_issuance = hydra_get_d(&reserves, amp); + + // Attacker deposits proportional to current reserves + let mut attacker_balances: Vec = reserves.iter().map(|r| r / deposit_fraction).collect(); + let initial_total: u128 = attacker_balances.iter().sum(); + + for _ in 0..iterations { + let new_reserves: Vec = reserves + .iter() + .zip(attacker_balances.iter()) + .map(|(r, a)| r + a) + .collect(); + + let shares_received = hydra_calc_shares(&reserves, &new_reserves, amp, share_issuance, fee); + if shares_received == 0 { + break; + } + + reserves = new_reserves; + share_issuance += shares_received; + + let mut withdrawn: Vec = Vec::new(); + for i in 0..n_assets { + let amount = calculate_liquidity_out(reserves[i], shares_received, share_issuance) + .expect("liquidity out failed"); + withdrawn.push(amount); + } + + for i in 0..n_assets { + reserves[i] -= withdrawn[i]; + } + share_issuance -= shares_received; + + attacker_balances = withdrawn; + } + + let final_total: u128 = attacker_balances.iter().sum(); + + eprintln!( + " {}: initial={} final={} diff={} ({})", + label, + initial_total, + final_total, + initial_total as i128 - final_total as i128, + if final_total <= initial_total { + "protocol safe" + } else { + "VALUE EXTRACTED" + } + ); + + assert!( + final_total <= initial_total, + "{}: attacker extracted value! initial={} final={} profit={}", + label, + initial_total, + final_total, + final_total - initial_total, + ); +} + +#[test] +fn curve_comparison_add_remove_cycle_imbalanced_2pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let fee = Permill::from_parts(500); // 0.05% + let no_fee = Permill::zero(); + + run_add_remove_cycle( + "2-pool 2:1 fee=0.05%", + vec![1_000_000_000_000_000_000, 500_000_000_000_000_000], + 100, fee, 10, 100, + ); + run_add_remove_cycle( + "2-pool 2:1 fee=0", + vec![1_000_000_000_000_000_000, 500_000_000_000_000_000], + 100, no_fee, 10, 1000, + ); + run_add_remove_cycle( + "2-pool 10:1 fee=0.05%", + vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], + 100, fee, 10, 100, + ); + run_add_remove_cycle( + "2-pool 10:1 fee=0", + vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], + 100, no_fee, 10, 1000, + ); + run_add_remove_cycle( + "2-pool 100:1 fee=0.05%", + vec![1_000_000_000_000_000_000, 10_000_000_000_000_000], + 100, fee, 10, 100, + ); + run_add_remove_cycle( + "2-pool 100:1 fee=0", + vec![1_000_000_000_000_000_000, 10_000_000_000_000_000], + 100, no_fee, 10, 1000, + ); + run_add_remove_cycle( + "2-pool 1000:1 fee=0", + vec![1_000_000_000_000_000_000, 1_000_000_000_000_000], + 100, no_fee, 10, 1000, + ); + }); +} + +#[test] +fn curve_comparison_add_remove_cycle_imbalanced_3pool() { + TestNet::reset(); + Hydra::execute_with(|| { + let fee = Permill::from_parts(500); + let no_fee = Permill::zero(); + + run_add_remove_cycle( + "3-pool [1:2:0.5] fee=0.05%", + vec![1_000_000_000_000_000_000, 2_000_000_000_000_000_000, 500_000_000_000_000_000], + 500, fee, 10, 100, + ); + run_add_remove_cycle( + "3-pool [1:2:0.5] fee=0", + vec![1_000_000_000_000_000_000, 2_000_000_000_000_000_000, 500_000_000_000_000_000], + 500, no_fee, 10, 1000, + ); + run_add_remove_cycle( + "3-pool [10:1:1] fee=0", + vec![10_000_000_000_000_000_000, 1_000_000_000_000_000_000, 1_000_000_000_000_000_000], + 100, no_fee, 10, 1000, + ); + run_add_remove_cycle( + "3-pool [1:1:0.01] fee=0", + vec![1_000_000_000_000_000_000, 1_000_000_000_000_000_000, 10_000_000_000_000_000], + 100, no_fee, 10, 1000, + ); + }); +} + +#[test] +fn curve_comparison_add_remove_cycle_imbalanced_varying_amp() { + TestNet::reset(); + Hydra::execute_with(|| { + let no_fee = Permill::zero(); + let reserves = vec![1_000_000_000_000_000_000u128, 100_000_000_000_000_000u128]; + + run_add_remove_cycle("10:1 amp=1 fee=0", reserves.clone(), 1, no_fee, 10, 1000); + run_add_remove_cycle("10:1 amp=100 fee=0", reserves.clone(), 100, no_fee, 10, 1000); + run_add_remove_cycle("10:1 amp=10000 fee=0", reserves.clone(), 10000, no_fee, 10, 1000); + }); +} + +#[test] +fn curve_comparison_add_remove_cycle_small_deposits_imbalanced() { + TestNet::reset(); + Hydra::execute_with(|| { + let no_fee = Permill::zero(); + + // Very small deposits — maximizes rounding impact relative to deposit size + run_add_remove_cycle( + "2-pool 10:1 tiny deposit fee=0", + vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], + 100, no_fee, 10000, 1000, + ); + run_add_remove_cycle( + "2-pool 10:1 micro deposit fee=0", + vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], + 100, no_fee, 1_000_000, 1000, + ); + }); +} From 1deaa0f13ecd7b1b486852af1cb28c594d08d143 Mon Sep 17 00:00:00 2001 From: mrq Date: Fri, 3 Apr 2026 18:51:13 +0200 Subject: [PATCH 3/4] fmt --- .../src/stableswap_curve_comparison.rs | 280 +++++++++++++++--- 1 file changed, 233 insertions(+), 47 deletions(-) diff --git a/integration-tests/src/stableswap_curve_comparison.rs b/integration-tests/src/stableswap_curve_comparison.rs index 215aba8b1e..541eba7159 100644 --- a/integration-tests/src/stableswap_curve_comparison.rs +++ b/integration-tests/src/stableswap_curve_comparison.rs @@ -286,9 +286,8 @@ fn hydra_calc_shares( let initial: Vec = old_balances.iter().map(|v| AssetReserve::new(*v, 18)).collect(); let updated: Vec = new_balances.iter().map(|v| AssetReserve::new(*v, 18)).collect(); let pegs: Vec<(u128, u128)> = vec![(1, 1); old_balances.len()]; - let (shares, _fees) = - calculate_shares::(&initial, &updated, amp, share_issuance, fee, &pegs) - .expect("hydra calculate_shares failed"); + let (shares, _fees) = calculate_shares::(&initial, &updated, amp, share_issuance, fee, &pegs) + .expect("hydra calculate_shares failed"); shares } @@ -781,9 +780,33 @@ fn curve_comparison_shares_with_fee_single_sided() { let amp = 100u128; let supply = 2_000_000_000_000_000_000u128; - run_shares_with_fee_comparison("single-sided 0.04%", contract, &old, &new, amp, supply, Permill::from_parts(400)); - run_shares_with_fee_comparison("single-sided 0.3%", contract, &old, &new, amp, supply, Permill::from_parts(3000)); - run_shares_with_fee_comparison("single-sided 1%", contract, &old, &new, amp, supply, Permill::from_parts(10000)); + run_shares_with_fee_comparison( + "single-sided 0.04%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(400), + ); + run_shares_with_fee_comparison( + "single-sided 0.3%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(3000), + ); + run_shares_with_fee_comparison( + "single-sided 1%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(10000), + ); }); } @@ -798,8 +821,24 @@ fn curve_comparison_shares_with_fee_balanced_deposit() { let supply = 2_000_000_000_000_000_000u128; // Balanced deposit should have near-zero fee impact - run_shares_with_fee_comparison("balanced 0.3%", contract, &old, &new, amp, supply, Permill::from_parts(3000)); - run_shares_with_fee_comparison("balanced 1%", contract, &old, &new, amp, supply, Permill::from_parts(10000)); + run_shares_with_fee_comparison( + "balanced 0.3%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(3000), + ); + run_shares_with_fee_comparison( + "balanced 1%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(10000), + ); }); } @@ -822,9 +861,33 @@ fn curve_comparison_shares_with_fee_3pool() { let amp = 2000u128; let supply = 3_000_000_000_000_000_000u128; - run_shares_with_fee_comparison("3-pool single-sided 0.04%", contract, &old, &new, amp, supply, Permill::from_parts(400)); - run_shares_with_fee_comparison("3-pool single-sided 0.3%", contract, &old, &new, amp, supply, Permill::from_parts(3000)); - run_shares_with_fee_comparison("3-pool single-sided 1%", contract, &old, &new, amp, supply, Permill::from_parts(10000)); + run_shares_with_fee_comparison( + "3-pool single-sided 0.04%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(400), + ); + run_shares_with_fee_comparison( + "3-pool single-sided 0.3%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(3000), + ); + run_shares_with_fee_comparison( + "3-pool single-sided 1%", + contract, + &old, + &new, + amp, + supply, + Permill::from_parts(10000), + ); }); } @@ -843,15 +906,10 @@ fn curve_comparison_withdraw_no_fee() { let amp = 100u128; let (curve_dy, _) = curve_calc_withdraw_one_coin(contract, &balances, withdraw_shares, 0, total_supply, amp, 0); - let (hydra_dy, _) = hydra_calc_withdraw_one_asset(&balances, withdraw_shares, 0, total_supply, amp, Permill::zero()); + let (hydra_dy, _) = + hydra_calc_withdraw_one_asset(&balances, withdraw_shares, 0, total_supply, amp, Permill::zero()); - assert_parity( - "withdraw no fee amount", - curve_dy, - hydra_dy, - MAX_SWAP_TOLERANCE, - false, - ); + assert_parity("withdraw no fee amount", curve_dy, hydra_dy, MAX_SWAP_TOLERANCE, false); }); } @@ -870,7 +928,8 @@ fn curve_comparison_withdraw_imbalanced_no_fee() { let amp = 500u128; let (curve_dy, _) = curve_calc_withdraw_one_coin(contract, &balances, withdraw_shares, 0, total_supply, amp, 0); - let (hydra_dy, _) = hydra_calc_withdraw_one_asset(&balances, withdraw_shares, 0, total_supply, amp, Permill::zero()); + let (hydra_dy, _) = + hydra_calc_withdraw_one_asset(&balances, withdraw_shares, 0, total_supply, amp, Permill::zero()); assert_parity( "withdraw imbalanced 3-pool no fee", @@ -924,9 +983,36 @@ fn curve_comparison_withdraw_with_fee_2pool() { let withdraw_shares = 100_000_000_000_000_000u128; let amp = 100u128; - run_withdraw_with_fee_comparison("2-pool 0.04%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(400)); - run_withdraw_with_fee_comparison("2-pool 0.3%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(3000)); - run_withdraw_with_fee_comparison("2-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); + run_withdraw_with_fee_comparison( + "2-pool 0.04%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(400), + ); + run_withdraw_with_fee_comparison( + "2-pool 0.3%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(3000), + ); + run_withdraw_with_fee_comparison( + "2-pool 1%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(10000), + ); }); } @@ -944,9 +1030,36 @@ fn curve_comparison_withdraw_with_fee_3pool() { let withdraw_shares = 150_000_000_000_000_000u128; // 5% let amp = 2000u128; - run_withdraw_with_fee_comparison("3-pool 0.04%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(400)); - run_withdraw_with_fee_comparison("3-pool 0.3%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(3000)); - run_withdraw_with_fee_comparison("3-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); + run_withdraw_with_fee_comparison( + "3-pool 0.04%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(400), + ); + run_withdraw_with_fee_comparison( + "3-pool 0.3%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(3000), + ); + run_withdraw_with_fee_comparison( + "3-pool 1%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(10000), + ); }); } @@ -964,8 +1077,26 @@ fn curve_comparison_withdraw_with_fee_imbalanced_3pool() { let withdraw_shares = 100_000_000_000_000_000u128; let amp = 500u128; - run_withdraw_with_fee_comparison("imbalanced 3-pool 0.3%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(3000)); - run_withdraw_with_fee_comparison("imbalanced 3-pool 1%", contract, &balances, withdraw_shares, 0, total_supply, amp, Permill::from_parts(10000)); + run_withdraw_with_fee_comparison( + "imbalanced 3-pool 0.3%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(3000), + ); + run_withdraw_with_fee_comparison( + "imbalanced 3-pool 1%", + contract, + &balances, + withdraw_shares, + 0, + total_supply, + amp, + Permill::from_parts(10000), + ); }); } @@ -1231,8 +1362,8 @@ fn run_add_remove_cycle( let mut withdrawn: Vec = Vec::new(); for i in 0..n_assets { - let amount = calculate_liquidity_out(reserves[i], shares_received, share_issuance) - .expect("liquidity out failed"); + let amount = + calculate_liquidity_out(reserves[i], shares_received, share_issuance).expect("liquidity out failed"); withdrawn.push(amount); } @@ -1279,37 +1410,58 @@ fn curve_comparison_add_remove_cycle_imbalanced_2pool() { run_add_remove_cycle( "2-pool 2:1 fee=0.05%", vec![1_000_000_000_000_000_000, 500_000_000_000_000_000], - 100, fee, 10, 100, + 100, + fee, + 10, + 100, ); run_add_remove_cycle( "2-pool 2:1 fee=0", vec![1_000_000_000_000_000_000, 500_000_000_000_000_000], - 100, no_fee, 10, 1000, + 100, + no_fee, + 10, + 1000, ); run_add_remove_cycle( "2-pool 10:1 fee=0.05%", vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], - 100, fee, 10, 100, + 100, + fee, + 10, + 100, ); run_add_remove_cycle( "2-pool 10:1 fee=0", vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], - 100, no_fee, 10, 1000, + 100, + no_fee, + 10, + 1000, ); run_add_remove_cycle( "2-pool 100:1 fee=0.05%", vec![1_000_000_000_000_000_000, 10_000_000_000_000_000], - 100, fee, 10, 100, + 100, + fee, + 10, + 100, ); run_add_remove_cycle( "2-pool 100:1 fee=0", vec![1_000_000_000_000_000_000, 10_000_000_000_000_000], - 100, no_fee, 10, 1000, + 100, + no_fee, + 10, + 1000, ); run_add_remove_cycle( "2-pool 1000:1 fee=0", vec![1_000_000_000_000_000_000, 1_000_000_000_000_000], - 100, no_fee, 10, 1000, + 100, + no_fee, + 10, + 1000, ); }); } @@ -1323,23 +1475,51 @@ fn curve_comparison_add_remove_cycle_imbalanced_3pool() { run_add_remove_cycle( "3-pool [1:2:0.5] fee=0.05%", - vec![1_000_000_000_000_000_000, 2_000_000_000_000_000_000, 500_000_000_000_000_000], - 500, fee, 10, 100, + vec![ + 1_000_000_000_000_000_000, + 2_000_000_000_000_000_000, + 500_000_000_000_000_000, + ], + 500, + fee, + 10, + 100, ); run_add_remove_cycle( "3-pool [1:2:0.5] fee=0", - vec![1_000_000_000_000_000_000, 2_000_000_000_000_000_000, 500_000_000_000_000_000], - 500, no_fee, 10, 1000, + vec![ + 1_000_000_000_000_000_000, + 2_000_000_000_000_000_000, + 500_000_000_000_000_000, + ], + 500, + no_fee, + 10, + 1000, ); run_add_remove_cycle( "3-pool [10:1:1] fee=0", - vec![10_000_000_000_000_000_000, 1_000_000_000_000_000_000, 1_000_000_000_000_000_000], - 100, no_fee, 10, 1000, + vec![ + 10_000_000_000_000_000_000, + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000, + ], + 100, + no_fee, + 10, + 1000, ); run_add_remove_cycle( "3-pool [1:1:0.01] fee=0", - vec![1_000_000_000_000_000_000, 1_000_000_000_000_000_000, 10_000_000_000_000_000], - 100, no_fee, 10, 1000, + vec![ + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000, + 10_000_000_000_000_000, + ], + 100, + no_fee, + 10, + 1000, ); }); } @@ -1367,12 +1547,18 @@ fn curve_comparison_add_remove_cycle_small_deposits_imbalanced() { run_add_remove_cycle( "2-pool 10:1 tiny deposit fee=0", vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], - 100, no_fee, 10000, 1000, + 100, + no_fee, + 10000, + 1000, ); run_add_remove_cycle( "2-pool 10:1 micro deposit fee=0", vec![1_000_000_000_000_000_000, 100_000_000_000_000_000], - 100, no_fee, 1_000_000, 1000, + 100, + no_fee, + 1_000_000, + 1000, ); }); } From f181c176a3cefae0e3fb0ec26c424c24b925b194 Mon Sep 17 00:00:00 2001 From: mrq Date: Fri, 3 Apr 2026 19:00:45 +0200 Subject: [PATCH 4/4] bump --- Cargo.lock | 2 +- integration-tests/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37ab8ce2d7..4ad9c96306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15646,7 +15646,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.75.1" +version = "1.76.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 1d0422bd68..dd4baa45e5 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.75.1" +version = "1.76.0" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021"