diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index 66a5350..07c2f47 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## [0.5.0] - 2026-08-11 + +### Fixed +- reCLAMM (v1 + v2) no longer panics on off-centre pools with small balances that truncate the invariant to zero; the swap path now returns `PoolError::ZeroInvariant`. +- QuantAMM weight interpolation and reCLAMM (v1 + v2) price-range updates no longer panic when `current_timestamp < last_update` / `last_timestamp` (e.g. backfill / reorg); they return `PoolError::TimestampBeforeLastUpdate`. +- Fixed-point math helpers (`mul_*_fixed`, `div_*_fixed`, `mul_div_up_fixed`) now detect `U256` overflow via `checked_mul` and return `Err(PoolError::MathOverflow)` instead of panicking or silently producing masked values. +- Removed crate-wide `.unwrap_or(U256::ZERO)` / `.unwrap_or(WAD)` (and related panic-on-err) masking of math-helper `Result`s; failures now propagate as `Err`. +- Stable `compute_invariant` / `compute_balance` no longer panic on a zero token balance (or zero invariant); they return `Err(PoolError::StableZeroBalance)` / `Err(PoolError::ZeroInvariant)`. All-zero balances still return `Ok(0)` to match the reference implementations. + +### Changed (BREAKING) +- Renamed `compute_and_charge_aggregate_swap_fees` → `compute_and_charge_aggregate_swap_fees_raw` to make clear it returns the fee in the token's raw units (not scaled-18). +- `compute_current_virtual_balances` and reCLAMM math helpers now return `Result<_, PoolError>` instead of bare tuples. +- Several helpers changed from bare return types to `Result<_, PoolError>`: `compute_fourth_root_price_ratio`, `compute_centeredness`, `compute_invariant` (reCLAMM v1/v2), `_convert_to_shares` / `_convert_to_assets`, `calculate_value_change_progress` / `interpolate_value` / `get_normalized_weights` (LBP), `calculate_block_normalised_weight` (QuantAMM, **pub**), and `gyro_pool_math_sqrt`. +- Behavioural change: inputs that previously produced wrong numeric values (via zero/WAD fallbacks) now surface as `Err`. Callers using `?` are unaffected; callers doing `.unwrap()` will crash where they previously consumed wrong data. +- Public APIs that returned `Result<_, String>` now return `Result<_, PoolError>`: reCLAMM/v2 `compute_out_given_in` / `compute_in_given_out`, `swap_reclamm_to_price`, `calculate_buffer_amounts`, and `erc4626_buffer_wrap_or_unwrap`. New variants: `ReClammNegativeAmountOut`, `BufferWrapAmountTooSmall`, `Erc4626ExceededMaxDeposit`, `Erc4626ExceededMaxMint`. Removed the `From` / `From<&str>` impls for `PoolError`. + ## [0.4.1] - 2025-11-20 ### Changed diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8f53534..048cf63 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "balancer-maths-rust" -version = "0.4.4" +version = "0.5.0" edition = "2021" description = "Balancer V3 mathematics library in Rust" license = "MIT" diff --git a/rust/src/common/errors.rs b/rust/src/common/errors.rs index cb1a5c8..a219d24 100644 --- a/rust/src/common/errors.rs +++ b/rust/src/common/errors.rs @@ -1,5 +1,6 @@ //! Custom error types for the Balancer maths library +use alloy_primitives::U256; use std::fmt; /// Errors that can occur during pool operations @@ -87,7 +88,31 @@ pub enum PoolError { /// Stable invariant didn't converge StableInvariantDidntConverge, + /// Stable math received a zero token balance (invariant undefined unless all balances are zero) + StableZeroBalance, + TokenAmountOutIsGreaterThanBalance, + + /// Quoting at a timestamp before the pool's last update (e.g. backfill / reorg) + TimestampBeforeLastUpdate, + + /// reCLAMM computed a negative amount out (invariant inconsistency) + ReClammNegativeAmountOut, + + /// ERC4626 buffer wrap amount below the minimum safe threshold + BufferWrapAmountTooSmall, + + /// ERC4626 deposit exceeds the vault's maxDeposit limit + Erc4626ExceededMaxDeposit { + requested: U256, + max: U256, + }, + + /// ERC4626 mint exceeds the vault's maxMint limit + Erc4626ExceededMaxMint { + requested: U256, + max: U256, + }, } impl fmt::Display for PoolError { @@ -132,23 +157,27 @@ impl fmt::Display for PoolError { PoolError::StableInvariantDidntConverge => { write!(f, "Stable invariant didn't converge") } + PoolError::StableZeroBalance => { + write!(f, "Stable math undefined for zero token balance") + } PoolError::TokenAmountOutIsGreaterThanBalance => { write!(f, "Token amount out is greater than balance") } + PoolError::TimestampBeforeLastUpdate => { + write!(f, "Timestamp is before the pool's last update") + } + PoolError::ReClammNegativeAmountOut => { + write!(f, "reClammMath: NegativeAmountOut") + } + PoolError::BufferWrapAmountTooSmall => write!(f, "wrapAmountTooSmall"), + PoolError::Erc4626ExceededMaxDeposit { requested, max } => { + write!(f, "ERC4626ExceededMaxDeposit {} {}", requested, max) + } + PoolError::Erc4626ExceededMaxMint { requested, max } => { + write!(f, "ERC4626ExceededMaxMint {} {}", requested, max) + } } } } impl std::error::Error for PoolError {} - -impl From for PoolError { - fn from(msg: String) -> Self { - PoolError::Custom(msg) - } -} - -impl From<&str> for PoolError { - fn from(msg: &str) -> Self { - PoolError::Custom(msg.to_string()) - } -} diff --git a/rust/src/common/maths.rs b/rust/src/common/maths.rs index ca9fce8..a359fdd 100644 --- a/rust/src/common/maths.rs +++ b/rust/src/common/maths.rs @@ -7,7 +7,7 @@ use alloy_primitives::U256; /// Multiply two U256s and round up pub fn mul_up_fixed(a: &U256, b: &U256) -> Result { - let product = a * b; + let product = a.checked_mul(*b).ok_or(PoolError::MathOverflow)?; if product.is_zero() { return Ok(U256::ZERO); } @@ -23,7 +23,7 @@ pub fn div_up_fixed(a: &U256, b: &U256) -> Result { /// Multiply two U256s and round down pub fn mul_down_fixed(a: &U256, b: &U256) -> Result { - let product = a * b; + let product = a.checked_mul(*b).ok_or(PoolError::MathOverflow)?; let result = product / WAD; Ok(result) } @@ -37,7 +37,7 @@ pub fn div_down_fixed(a: &U256, b: &U256) -> Result { return Err(PoolError::MathOverflow); } - let a_inflated = a * WAD; + let a_inflated = a.checked_mul(WAD).ok_or(PoolError::MathOverflow)?; let result = a_inflated / b; Ok(result) } @@ -53,7 +53,7 @@ pub fn div_up(a: &U256, b: &U256) -> Result { /// Multiply and divide with up rounding pub fn mul_div_up_fixed(a: &U256, b: &U256, c: &U256) -> Result { - let product = a * b; + let product = a.checked_mul(*b).ok_or(PoolError::MathOverflow)?; if product.is_zero() { return Ok(U256::ZERO); } @@ -129,3 +129,51 @@ pub fn complement_fixed(x: &U256) -> Result { Ok(U256::ZERO) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mul_down_fixed_overflow_returns_err() { + let a = U256::MAX; + let b = U256::from(2u64); + assert!(matches!( + mul_down_fixed(&a, &b), + Err(PoolError::MathOverflow) + )); + } + + #[test] + fn mul_up_fixed_overflow_returns_err() { + let a = U256::MAX; + let b = U256::from(2u64); + assert!(matches!(mul_up_fixed(&a, &b), Err(PoolError::MathOverflow))); + } + + #[test] + fn mul_div_up_fixed_overflow_returns_err() { + let a = U256::MAX; + let b = U256::from(2u64); + assert!(matches!( + mul_div_up_fixed(&a, &b, &WAD), + Err(PoolError::MathOverflow) + )); + } + + #[test] + fn div_down_fixed_inflated_overflow_returns_err() { + // a * WAD overflows when a is large enough + let a = U256::MAX; + assert!(matches!( + div_down_fixed(&a, &U256::from(1u64)), + Err(PoolError::MathOverflow) + )); + } + + #[test] + fn mul_down_fixed_normal_ok() { + let result = mul_down_fixed(&WAD, &WAD).unwrap(); + assert_eq!(result, WAD); + } +} diff --git a/rust/src/common/mod.rs b/rust/src/common/mod.rs index 1aa23cb..adcc8cf 100644 --- a/rust/src/common/mod.rs +++ b/rust/src/common/mod.rs @@ -19,7 +19,7 @@ pub use types::{ Rounding, SwapInput, SwapKind, SwapParams, SwapResult, }; pub use utils::{ - compute_and_charge_aggregate_swap_fees, copy_to_scaled18_apply_rate_round_down_array, + compute_and_charge_aggregate_swap_fees_raw, copy_to_scaled18_apply_rate_round_down_array, copy_to_scaled18_apply_rate_round_up_array, find_case_insensitive_index_in_list, get_single_input_index, is_same_address, require_unbalanced_liquidity_enabled, to_raw_undo_rate_round_down, to_raw_undo_rate_round_up, to_scaled_18_apply_rate_round_down, diff --git a/rust/src/common/utils.rs b/rust/src/common/utils.rs index 6fd87b9..033f775 100644 --- a/rust/src/common/utils.rs +++ b/rust/src/common/utils.rs @@ -105,8 +105,10 @@ pub fn copy_to_scaled18_apply_rate_round_up_array( Ok(scaled_amounts) } -/// Compute and charge aggregate swap fees -pub fn compute_and_charge_aggregate_swap_fees( +/// Compute and charge aggregate swap fees. +/// +/// Returns the aggregate swap fee amount in the token's raw units (not scaled-18). +pub fn compute_and_charge_aggregate_swap_fees_raw( swap_fee_amount_scaled18: &U256, aggregate_swap_fee_percentage: &U256, decimal_scaling_factors: &[U256], diff --git a/rust/src/hooks/akron/mod.rs b/rust/src/hooks/akron/mod.rs index 2a52044..84cea29 100644 --- a/rust/src/hooks/akron/mod.rs +++ b/rust/src/hooks/akron/mod.rs @@ -117,31 +117,53 @@ impl HookBase for AkronHook { match hook_state { HookState::Akron(state) => { let calculated_swap_fee_percentage = if swap_params.swap_kind == SwapKind::GivenIn { - let exponent = div_down_fixed( + let Ok(exponent) = div_down_fixed( &state.weights[swap_params.token_in_index], &state.weights[swap_params.token_out_index], - ) - .unwrap_or(U256::ZERO); + ) else { + return DynamicSwapFeeResult { + success: false, + dynamic_swap_fee: U256::ZERO, + }; + }; - Self::compute_swap_fee_percentage_given_exact_in( + match Self::compute_swap_fee_percentage_given_exact_in( &swap_params.balances_live_scaled_18[swap_params.token_in_index], &exponent, &swap_params.amount_scaled_18, - ) - .unwrap_or(U256::ZERO) + ) { + Ok(fee) => fee, + Err(_) => { + return DynamicSwapFeeResult { + success: false, + dynamic_swap_fee: U256::ZERO, + }; + } + } } else { - let exponent = div_up_fixed( + let Ok(exponent) = div_up_fixed( &state.weights[swap_params.token_out_index], &state.weights[swap_params.token_in_index], - ) - .unwrap_or(U256::ZERO); + ) else { + return DynamicSwapFeeResult { + success: false, + dynamic_swap_fee: U256::ZERO, + }; + }; - Self::compute_swap_fee_percentage_given_exact_out( + match Self::compute_swap_fee_percentage_given_exact_out( &swap_params.balances_live_scaled_18[swap_params.token_out_index], &exponent, &swap_params.amount_scaled_18, - ) - .unwrap_or(U256::ZERO) + ) { + Ok(fee) => fee, + Err(_) => { + return DynamicSwapFeeResult { + success: false, + dynamic_swap_fee: U256::ZERO, + }; + } + } }; // Charge the static or calculated fee, whichever is greater diff --git a/rust/src/hooks/exit_fee/mod.rs b/rust/src/hooks/exit_fee/mod.rs index e245749..50a2b11 100644 --- a/rust/src/hooks/exit_fee/mod.rs +++ b/rust/src/hooks/exit_fee/mod.rs @@ -88,11 +88,15 @@ impl HookBase for ExitFeeHook { if state.remove_liquidity_hook_fee_percentage > U256::ZERO { // Charge fees proportional to amounts out of each token for i in 0..amounts_out_raw.len() { - let hook_fee = mul_down_fixed( + let Ok(hook_fee) = mul_down_fixed( &amounts_out_raw[i], &state.remove_liquidity_hook_fee_percentage, - ) - .unwrap_or(U256::ZERO); + ) else { + return AfterRemoveLiquidityResult { + success: false, + hook_adjusted_amounts_out_raw: amounts_out_raw.to_vec(), + }; + }; accrued_fees[i] = hook_fee; hook_adjusted_amounts_out_raw[i] -= hook_fee; diff --git a/rust/src/pools/buffer/buffer_math.rs b/rust/src/pools/buffer/buffer_math.rs index a75a87f..566cda2 100644 --- a/rust/src/pools/buffer/buffer_math.rs +++ b/rust/src/pools/buffer/buffer_math.rs @@ -1,3 +1,4 @@ +use crate::common::errors::PoolError; use crate::common::maths::{div_down_fixed, div_up_fixed, mul_down_fixed, mul_up_fixed}; use crate::common::types::{Rounding, SwapKind}; use crate::pools::buffer::enums::WrappingDirection; @@ -22,7 +23,7 @@ pub fn calculate_buffer_amounts( rate: &U256, max_deposit: Option<&U256>, max_mint: Option<&U256>, -) -> Result { +) -> Result { match direction { WrappingDirection::Wrap => { // Amount in is underlying tokens, amount out is wrapped tokens @@ -31,24 +32,23 @@ pub fn calculate_buffer_amounts( // previewDeposit let max_assets = max_deposit.unwrap_or(&U256::MAX); if amount_raw > max_assets { - return Err(format!( - "ERC4626ExceededMaxDeposit {} {}", - amount_raw, max_assets - )); + return Err(PoolError::Erc4626ExceededMaxDeposit { + requested: *amount_raw, + max: *max_assets, + }); } - Ok(_convert_to_shares(amount_raw, rate, Rounding::RoundDown)) + _convert_to_shares(amount_raw, rate, Rounding::RoundDown) } SwapKind::GivenOut => { // previewMint let max_shares = max_mint.unwrap_or(&U256::MAX); if amount_raw > max_shares { - return Err(format!( - "ERC4626ExceededMaxMint {} {}", - amount_raw, - max_mint.unwrap_or(&U256::ZERO) - )); + return Err(PoolError::Erc4626ExceededMaxMint { + requested: *amount_raw, + max: *max_mint.unwrap_or(&U256::ZERO), + }); } - Ok(_convert_to_assets(amount_raw, rate, Rounding::RoundUp)) + _convert_to_assets(amount_raw, rate, Rounding::RoundUp) } } } @@ -57,11 +57,11 @@ pub fn calculate_buffer_amounts( match kind { SwapKind::GivenIn => { // previewRedeem - Ok(_convert_to_assets(amount_raw, rate, Rounding::RoundDown)) + _convert_to_assets(amount_raw, rate, Rounding::RoundDown) } SwapKind::GivenOut => { // previewWithdraw - Ok(_convert_to_shares(amount_raw, rate, Rounding::RoundUp)) + _convert_to_shares(amount_raw, rate, Rounding::RoundUp) } } } @@ -69,17 +69,17 @@ pub fn calculate_buffer_amounts( } /// Convert assets to shares -fn _convert_to_shares(assets: &U256, rate: &U256, rounding: Rounding) -> U256 { +fn _convert_to_shares(assets: &U256, rate: &U256, rounding: Rounding) -> Result { match rounding { - Rounding::RoundUp => div_up_fixed(assets, rate).unwrap_or(U256::ZERO), - Rounding::RoundDown => div_down_fixed(assets, rate).unwrap_or(U256::ZERO), + Rounding::RoundUp => div_up_fixed(assets, rate), + Rounding::RoundDown => div_down_fixed(assets, rate), } } /// Convert shares to assets -fn _convert_to_assets(shares: &U256, rate: &U256, rounding: Rounding) -> U256 { +fn _convert_to_assets(shares: &U256, rate: &U256, rounding: Rounding) -> Result { match rounding { - Rounding::RoundUp => mul_up_fixed(shares, rate).unwrap_or(U256::ZERO), - Rounding::RoundDown => mul_down_fixed(shares, rate).unwrap_or(U256::ZERO), + Rounding::RoundUp => mul_up_fixed(shares, rate), + Rounding::RoundDown => mul_down_fixed(shares, rate), } } diff --git a/rust/src/pools/buffer/erc4626_buffer_wrap_or_unwrap.rs b/rust/src/pools/buffer/erc4626_buffer_wrap_or_unwrap.rs index adb3078..5cbe9c9 100644 --- a/rust/src/pools/buffer/erc4626_buffer_wrap_or_unwrap.rs +++ b/rust/src/pools/buffer/erc4626_buffer_wrap_or_unwrap.rs @@ -1,5 +1,6 @@ //! ERC4626 Buffer wrap or unwrap function +use crate::common::errors::PoolError; use crate::common::types::SwapInput; use crate::pools::buffer::buffer_data::BufferState; use crate::pools::buffer::buffer_math::calculate_buffer_amounts; @@ -19,12 +20,12 @@ pub const _MINIMUM_WRAP_AMOUNT: U256 = uint!(1000_U256); pub fn erc4626_buffer_wrap_or_unwrap( swap_input: &SwapInput, pool_state: &BufferState, -) -> Result { +) -> Result { if swap_input.amount_raw < _MINIMUM_WRAP_AMOUNT { // If amount given is too small, rounding issues can be introduced that favors the user and can drain // the buffer. _MINIMUM_WRAP_AMOUNT prevents it. Most tokens have protections against it already, this // is just an extra layer of security. - return Err("wrapAmountTooSmall".to_string()); + return Err(PoolError::BufferWrapAmountTooSmall); } // Determine wrapping direction based on token addresses diff --git a/rust/src/pools/gyro/gyro_eclp_math.rs b/rust/src/pools/gyro/gyro_eclp_math.rs index 079ca7f..7383921 100644 --- a/rust/src/pools/gyro/gyro_eclp_math.rs +++ b/rust/src/pools/gyro/gyro_eclp_math.rs @@ -212,7 +212,12 @@ fn calc_a_chi_a_chi_in_xp(p: &EclpParams, d: &DerivedEclpParams) -> I256 { val } -fn calc_invariant_sqrt(x: &I256, y: &I256, p: &EclpParams, d: &DerivedEclpParams) -> (I256, I256) { +fn calc_invariant_sqrt( + x: &I256, + y: &I256, + p: &EclpParams, + d: &DerivedEclpParams, +) -> Result<(I256, I256), PoolError> { let val1 = calc_min_atx_a_chiy_sq_plus_atx_sq(x, y, p, d); let val2 = calc_2_atx_aty_a_chix_a_chiy(x, y, p, d); let val3 = calc_min_aty_a_chix_sq_plus_aty_sq(x, y, p, d); @@ -223,13 +228,13 @@ fn calc_invariant_sqrt(x: &I256, y: &I256, p: &EclpParams, d: &DerivedEclpParams let val = if val > I256::ZERO { // Convert to U256 for sqrt, then back to I256 let val_u256 = val.into_raw(); - let sqrt_result = gyro_pool_math_sqrt(&val_u256, 5); + let sqrt_result = gyro_pool_math_sqrt(&val_u256, 5)?; I256::from_raw(sqrt_result) } else { I256::ZERO }; - (val, err) + Ok((val, err)) } fn calc_min_atx_a_chiy_sq_plus_atx_sq( @@ -415,7 +420,7 @@ pub fn calculate_invariant_with_error( } let at_a_chi = calc_at_a_chi(&x, &y, params, derived); - let invariant_result = calc_invariant_sqrt(&x, &y, params, derived); + let invariant_result = calc_invariant_sqrt(&x, &y, params, derived)?; let sqrt = invariant_result.0; let mut err = invariant_result.1; @@ -428,7 +433,7 @@ pub fn calculate_invariant_with_error( // O(1e-18) err = if err > I256::ZERO { let err_u256 = err.into_raw(); - let sqrt_result = gyro_pool_math_sqrt(&err_u256, 5); + let sqrt_result = gyro_pool_math_sqrt(&err_u256, 5)?; I256::from_raw(sqrt_result) } else { I256::from_str("1000000000").unwrap() @@ -485,7 +490,7 @@ fn solve_quadratic_swap( ab: &Vector2, tau_beta: &Vector2, d_sq: &I256, -) -> I256 { +) -> Result { let lam_bar_x_result = ONE_XP - div_down_mag(&div_down_mag(&ONE_XP, lambda), lambda); let lam_bar = Vector2 { x: lam_bar_x_result, @@ -529,7 +534,7 @@ fn solve_quadratic_swap( q.c = if q.c > I256::ZERO { let q_c_u256 = q.c.into_raw(); - let sqrt_result = gyro_pool_math_sqrt(&q_c_u256, 5); + let sqrt_result = gyro_pool_math_sqrt(&q_c_u256, 5)?; I256::from_raw(sqrt_result) } else { I256::ZERO @@ -541,7 +546,7 @@ fn solve_quadratic_swap( q.a = mul_up_xp_to_np(&(q.b - q.c), &div_xp_u(&ONE_XP, &s_term_x)); } - q.a + ab.y + Ok(q.a + ab.y) } fn calc_xp_xp_div_lambda_lambda( @@ -630,7 +635,12 @@ fn calc_xp_xp_div_lambda_lambda( mul_up_xp_to_np(&val, &term_xp2) + q.a } -fn calc_y_given_x(x: &I256, params: &EclpParams, d: &DerivedEclpParams, r: &Vector2) -> I256 { +fn calc_y_given_x( + x: &I256, + params: &EclpParams, + d: &DerivedEclpParams, + r: &Vector2, +) -> Result { let ab = Vector2 { x: virtual_offset0(params, d, r), y: virtual_offset1(params, d, r), @@ -647,7 +657,12 @@ fn calc_y_given_x(x: &I256, params: &EclpParams, d: &DerivedEclpParams, r: &Vect ) } -fn calc_x_given_y(y: &I256, params: &EclpParams, d: &DerivedEclpParams, r: &Vector2) -> I256 { +fn calc_x_given_y( + y: &I256, + params: &EclpParams, + d: &DerivedEclpParams, + r: &Vector2, +) -> Result { let ba = Vector2 { x: virtual_offset1(params, d, r), y: virtual_offset0(params, d, r), @@ -699,13 +714,13 @@ pub fn calc_out_given_in( let bal_in_new = if token_in_is_token0 { let bal_in_new_signed = I256::from_raw(balances[0] + amount_in); check_asset_bounds(params, derived, invariant, &bal_in_new_signed, 0)?; - let bal_out_new = calc_y_given_x(&bal_in_new_signed, params, derived, invariant); + let bal_out_new = calc_y_given_x(&bal_in_new_signed, params, derived, invariant)?; let bal_out_new_u256 = bal_out_new.into_raw(); balances[1] - bal_out_new_u256 } else { let bal_in_new_signed = I256::from_raw(balances[1] + amount_in); check_asset_bounds(params, derived, invariant, &bal_in_new_signed, 1)?; - let bal_out_new = calc_x_given_y(&bal_in_new_signed, params, derived, invariant); + let bal_out_new = calc_x_given_y(&bal_in_new_signed, params, derived, invariant)?; let bal_out_new_u256 = bal_out_new.into_raw(); balances[0] - bal_out_new_u256 }; @@ -725,7 +740,7 @@ pub fn calc_in_given_out( return Err(PoolError::InvalidInput("Asset bounds exceeded".to_string())); } let bal_out_new_signed = I256::from_raw(balances[1] - amount_out); - let bal_in_new = calc_x_given_y(&bal_out_new_signed, params, derived, invariant); + let bal_in_new = calc_x_given_y(&bal_out_new_signed, params, derived, invariant)?; check_asset_bounds(params, derived, invariant, &bal_in_new, 0)?; let bal_in_new_u256 = bal_in_new.into_raw(); Ok(bal_in_new_u256 - balances[0]) @@ -734,7 +749,7 @@ pub fn calc_in_given_out( return Err(PoolError::InvalidInput("Asset bounds exceeded".to_string())); } let bal_out_new_signed = I256::from_raw(balances[0] - amount_out); - let bal_in_new = calc_y_given_x(&bal_out_new_signed, params, derived, invariant); + let bal_in_new = calc_y_given_x(&bal_out_new_signed, params, derived, invariant)?; check_asset_bounds(params, derived, invariant, &bal_in_new, 1)?; let bal_in_new_u256 = bal_in_new.into_raw(); Ok(bal_in_new_u256 - balances[1]) @@ -784,11 +799,11 @@ pub fn compute_balance( if token_index == 0 { let balance1_signed = I256::from_raw(balances[1]); - let result = calc_x_given_y(&balance1_signed, params, derived, &invariant); + let result = calc_x_given_y(&balance1_signed, params, derived, &invariant)?; Ok(result.into_raw()) } else { let balance0_signed = I256::from_raw(balances[0]); - let result = calc_y_given_x(&balance0_signed, params, derived, &invariant); + let result = calc_y_given_x(&balance0_signed, params, derived, &invariant)?; Ok(result.into_raw()) } } diff --git a/rust/src/pools/gyro/gyro_pool_math.rs b/rust/src/pools/gyro/gyro_pool_math.rs index 1656f8a..f361480 100644 --- a/rust/src/pools/gyro/gyro_pool_math.rs +++ b/rust/src/pools/gyro/gyro_pool_math.rs @@ -1,4 +1,5 @@ use crate::common::constants::WAD; +use crate::common::errors::PoolError; use crate::common::maths::{mul_down_fixed, mul_up_fixed}; use alloy_primitives::{uint, U256}; @@ -15,9 +16,9 @@ pub const SQRT_1E_NEG_17: U256 = uint!(3162277660_U256); /// Implements a square root algorithm using Newton's method and a first-guess optimization. /// Based on the Python implementation in gyro_pool_math.py -pub fn gyro_pool_math_sqrt(x: &U256, tolerance: u64) -> U256 { +pub fn gyro_pool_math_sqrt(x: &U256, tolerance: u64) -> Result { if x.is_zero() { - return U256::ZERO; + return Ok(U256::ZERO); } let mut guess = make_initial_guess(x); @@ -28,20 +29,19 @@ pub fn gyro_pool_math_sqrt(x: &U256, tolerance: u64) -> U256 { } // Check that squaredGuess (guess * guess) is close enough from input - let guess_squared = - mul_down_fixed(&guess, &guess).unwrap_or_else(|_| panic!("mul_down_fixed failed")); + let guess_squared = mul_down_fixed(&guess, &guess)?; let tolerance_bigint = U256::from(tolerance); - let upper_bound = x + mul_up_fixed(&guess, &tolerance_bigint) - .unwrap_or_else(|_| panic!("mul_up_fixed failed")); - let lower_bound = x - mul_up_fixed(&guess, &tolerance_bigint) - .unwrap_or_else(|_| panic!("mul_up_fixed failed")); + let upper_bound = x + mul_up_fixed(&guess, &tolerance_bigint)?; + let lower_bound = x - mul_up_fixed(&guess, &tolerance_bigint)?; if !(guess_squared <= upper_bound && guess_squared >= lower_bound) { - panic!("_sqrt FAILED"); + return Err(PoolError::InvalidInput( + "gyro_pool_math_sqrt failed to converge".to_string(), + )); } - guess + Ok(guess) } /// Makes an initial guess for the square root calculation diff --git a/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_math.rs b/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_math.rs index 7865988..29215c7 100644 --- a/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_math.rs +++ b/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_math.rs @@ -1,4 +1,5 @@ use crate::common::constants::WAD; +use crate::common::errors::PoolError; use crate::common::maths::{div_down_fixed, mul_down_fixed}; use alloy_primitives::U256; @@ -21,7 +22,7 @@ pub fn get_normalized_weights( end_time: &U256, project_token_start_weight: &U256, project_token_end_weight: &U256, -) -> Vec { +) -> Result, PoolError> { let mut normalized_weights = vec![U256::ZERO; 2]; // Infer the reserve token index @@ -34,12 +35,12 @@ pub fn get_normalized_weights( end_time, project_token_start_weight, project_token_end_weight, - ); + )?; // Calculate the normalized weight for the reserve token normalized_weights[reserve_token_index] = WAD - normalized_weights[project_token_index]; - normalized_weights + Ok(normalized_weights) } /// Calculate the normalized weight of the project token @@ -49,8 +50,8 @@ fn get_project_token_normalized_weight( end_time: &U256, start_weight: &U256, end_weight: &U256, -) -> U256 { - let pct_progress = calculate_value_change_progress(current_time, start_time, end_time); +) -> Result { + let pct_progress = calculate_value_change_progress(current_time, start_time, end_time)?; interpolate_value(start_weight, end_weight, &pct_progress) } @@ -59,34 +60,38 @@ fn calculate_value_change_progress( current_time: &U256, start_time: &U256, end_time: &U256, -) -> U256 { +) -> Result { if current_time >= end_time { - return WAD; // Fully completed + return Ok(WAD); // Fully completed } else if current_time <= start_time { - return U256::ZERO; // Not started + return Ok(U256::ZERO); // Not started } let total_seconds = end_time - start_time; let seconds_elapsed = current_time - start_time; - div_down_fixed(&seconds_elapsed, &total_seconds).unwrap_or(U256::ZERO) + div_down_fixed(&seconds_elapsed, &total_seconds) } /// Interpolate a value based on the progress of a change -fn interpolate_value(start_value: &U256, end_value: &U256, pct_progress: &U256) -> U256 { +fn interpolate_value( + start_value: &U256, + end_value: &U256, + pct_progress: &U256, +) -> Result { if pct_progress >= &WAD || start_value == end_value { - return *end_value; + return Ok(*end_value); } if pct_progress == &U256::ZERO { - return *start_value; + return Ok(*start_value); } if start_value > end_value { - let delta = mul_down_fixed(pct_progress, &(start_value - end_value)).unwrap_or(U256::ZERO); - start_value - delta + let delta = mul_down_fixed(pct_progress, &(start_value - end_value))?; + Ok(start_value - delta) } else { - let delta = mul_down_fixed(pct_progress, &(end_value - start_value)).unwrap_or(U256::ZERO); - start_value + delta + let delta = mul_down_fixed(pct_progress, &(end_value - start_value))?; + Ok(start_value + delta) } } diff --git a/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_pool.rs b/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_pool.rs index c825e6f..e22e256 100644 --- a/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_pool.rs +++ b/rust/src/pools/liquidity_bootstrapping/liquidity_bootstrapping_pool.rs @@ -31,7 +31,7 @@ impl LiquidityBootstrappingPool { &state.immutable.end_time, &state.immutable.start_weights[state.immutable.project_token_index], &state.immutable.end_weights[state.immutable.project_token_index], - ); + )?; Ok(Self { normalized_weights, diff --git a/rust/src/pools/quantamm/quantamm_math.rs b/rust/src/pools/quantamm/quantamm_math.rs index 9cf35a4..d94e8ff 100644 --- a/rust/src/pools/quantamm/quantamm_math.rs +++ b/rust/src/pools/quantamm/quantamm_math.rs @@ -1,3 +1,4 @@ +use crate::common::errors::PoolError; use crate::common::maths::mul_down_fixed; use alloy_primitives::{uint, I256, U256}; @@ -17,18 +18,16 @@ pub fn calculate_block_normalised_weight( weight: &I256, multiplier: &I256, time_since_last_update: &U256, -) -> U256 { +) -> Result { // multiplier is always below 1, we multiply by 1e18 for rounding let multiplier_scaled18 = *multiplier * ONE_SIGNED; if multiplier > &I256::ZERO { - weight.into_raw() - + mul_down_fixed(&multiplier_scaled18.into_raw(), time_since_last_update) - .unwrap_or(U256::ZERO) + Ok(weight.into_raw() + + mul_down_fixed(&multiplier_scaled18.into_raw(), time_since_last_update)?) } else { - weight.into_raw() - - mul_down_fixed(&(-multiplier_scaled18.into_raw()), time_since_last_update) - .unwrap_or(U256::ZERO) + Ok(weight.into_raw() + - mul_down_fixed(&(-multiplier_scaled18.into_raw()), time_since_last_update)?) } } diff --git a/rust/src/pools/quantamm/quantamm_pool.rs b/rust/src/pools/quantamm/quantamm_pool.rs index eea2e14..9d1df67 100644 --- a/rust/src/pools/quantamm/quantamm_pool.rs +++ b/rust/src/pools/quantamm/quantamm_pool.rs @@ -47,7 +47,7 @@ impl QuantAmmPool { &state.mutable.last_update_time, &state.mutable.last_interop_time, &state.mutable.current_timestamp, - ); + )?; Ok(Self { normalized_weights, @@ -62,14 +62,16 @@ impl QuantAmmPool { last_update_time: &U256, last_interop_time: &U256, current_timestamp: &U256, - ) -> Vec { + ) -> Result, PoolError> { let mut multiplier_time = *current_timestamp; if current_timestamp >= last_interop_time { multiplier_time = *last_interop_time; } - let time_since_last_update = multiplier_time - last_update_time; + let time_since_last_update = multiplier_time + .checked_sub(*last_update_time) + .ok_or(PoolError::TimestampBeforeLastUpdate)?; let mut normalized_weights = Vec::with_capacity(base_weights.len()); @@ -78,11 +80,11 @@ impl QuantAmmPool { &base_weights[i], &multipliers[i], &time_since_last_update, - ); + )?; normalized_weights.push(normalized_weight); } - normalized_weights + Ok(normalized_weights) } /// Get normalized weights for a specific token pair @@ -110,8 +112,7 @@ impl QuantAmmPool { let max_amount = mul_down_fixed( balance_scaled_18, &self.state.immutable.max_trade_size_ratio, - ) - .unwrap_or(U256::ZERO); + )?; if amount_scaled_18 > &max_amount { return Err(PoolError::InvalidSwapParameters); diff --git a/rust/src/pools/reclamm/reclamm_math.rs b/rust/src/pools/reclamm/reclamm_math.rs index ec5314e..0adbe3f 100644 --- a/rust/src/pools/reclamm/reclamm_math.rs +++ b/rust/src/pools/reclamm/reclamm_math.rs @@ -1,4 +1,5 @@ use crate::common::constants::{RAY, TWO_WAD}; +use crate::common::errors::PoolError; use crate::common::log_exp_math::pow; use crate::common::maths::{div_down_fixed, div_up_fixed, mul_down_fixed, mul_up_fixed}; use crate::common::oz_math::sqrt; @@ -24,9 +25,9 @@ pub fn compute_current_virtual_balances( end_fourth_root_price_ratio: &U256, price_ratio_update_start_time: &U256, price_ratio_update_end_time: &U256, -) -> (U256, U256, bool) { +) -> Result<(U256, U256, bool), PoolError> { if last_timestamp == current_timestamp { - return (*last_virtual_balance_a, *last_virtual_balance_b, false); + return Ok((*last_virtual_balance_a, *last_virtual_balance_b, false)); } let mut current_virtual_balance_a = *last_virtual_balance_a; @@ -38,7 +39,7 @@ pub fn compute_current_virtual_balances( end_fourth_root_price_ratio, price_ratio_update_start_time, price_ratio_update_end_time, - ); + )?; let mut changed = false; @@ -52,7 +53,7 @@ pub fn compute_current_virtual_balances( balances_scaled_18, last_virtual_balance_a, last_virtual_balance_b, - ); + )?; current_virtual_balance_a = new_virtual_balance_a; current_virtual_balance_b = new_virtual_balance_b; changed = true; @@ -62,7 +63,7 @@ pub fn compute_current_virtual_balances( balances_scaled_18, ¤t_virtual_balance_a, ¤t_virtual_balance_b, - ); + )?; // If the pool is outside the target range, track the market price by moving the price interval. if centeredness < *centeredness_margin { @@ -75,17 +76,17 @@ pub fn compute_current_virtual_balances( daily_price_shift_base, current_timestamp, last_timestamp, - ); + )?; current_virtual_balance_a = new_virtual_balance_a; current_virtual_balance_b = new_virtual_balance_b; changed = true; } - ( + Ok(( current_virtual_balance_a, current_virtual_balance_b, changed, - ) + )) } /// Calculate virtual balances when updating price ratio using Bhaskara formula @@ -94,13 +95,13 @@ fn calculate_virtual_balances_updating_price_ratio( balances_scaled_18: &[U256], last_virtual_balance_a: &U256, last_virtual_balance_b: &U256, -) -> (U256, U256) { +) -> Result<(U256, U256), PoolError> { // Compute the current pool centeredness, which will remain constant. let (pool_centeredness, is_pool_above_center) = compute_centeredness( balances_scaled_18, last_virtual_balance_a, last_virtual_balance_b, - ); + )?; // The overvalued token is the one with a lower token balance (therefore, rarer and more valuable). let ( @@ -132,28 +133,38 @@ fn calculate_virtual_balances_updating_price_ratio( let sqrt_price_ratio = mul_down_fixed( current_fourth_root_price_ratio, current_fourth_root_price_ratio, - ) - .unwrap_or(U256::ZERO); + )?; // Using FixedPoint math as little as possible to improve the precision of the result. // Note: The input of sqrt must be a 36-decimal number, so that the final result is 18 decimals. - let sqrt_input = pool_centeredness - * (pool_centeredness + (U256::from(4) * sqrt_price_ratio) - TWO_WAD) - + RAY; + let centeredness_inner = (pool_centeredness + (U256::from(4) * sqrt_price_ratio)) + .checked_sub(TWO_WAD) + .ok_or(PoolError::MathOverflow)?; + let sqrt_input = pool_centeredness * centeredness_inner + RAY; let sqrt_result = sqrt(&sqrt_input); - let virtual_balance_undervalued = balance_token_undervalued - * (WAD + pool_centeredness + sqrt_result) - / (U256::from(2) * (sqrt_price_ratio - WAD)); + let q0_minus_one = sqrt_price_ratio + .checked_sub(WAD) + .ok_or(PoolError::MathOverflow)?; + let denominator = U256::from(2) * q0_minus_one; + if denominator.is_zero() { + return Err(PoolError::ZeroInvariant); + } + + let virtual_balance_undervalued = + balance_token_undervalued * (WAD + pool_centeredness + sqrt_result) / denominator; + if last_virtual_balance_undervalued.is_zero() { + return Err(PoolError::ZeroInvariant); + } let virtual_balance_overvalued = (virtual_balance_undervalued * last_virtual_balance_overvalued) / last_virtual_balance_undervalued; if is_pool_above_center { - (virtual_balance_undervalued, virtual_balance_overvalued) + Ok((virtual_balance_undervalued, virtual_balance_overvalued)) } else { - (virtual_balance_overvalued, virtual_balance_undervalued) + Ok((virtual_balance_overvalued, virtual_balance_undervalued)) } } @@ -166,9 +177,15 @@ fn compute_virtual_balances_updating_price_range( daily_price_shift_base: &U256, current_timestamp: &U256, last_timestamp: &U256, -) -> (U256, U256) { +) -> Result<(U256, U256), PoolError> { + // Fail before price-ratio math when quoting a block older than the last update. + let time_difference = current_timestamp + .checked_sub(*last_timestamp) + .ok_or(PoolError::TimestampBeforeLastUpdate)?; + let time_difference_wad = time_difference * WAD; + let sqrt_price_ratio = sqrt( - &(compute_price_ratio(balances_scaled_18, virtual_balance_a, virtual_balance_b) * WAD), + &(compute_price_ratio(balances_scaled_18, virtual_balance_a, virtual_balance_b)? * WAD), ); let (balances_scaled_undervalued, balances_scaled_overvalued) = if is_pool_above_center { @@ -184,27 +201,28 @@ fn compute_virtual_balances_updating_price_range( }; // Vb = Vb * (dailyPriceShiftBase)^(T_curr - T_last) - let time_difference = current_timestamp - last_timestamp; - let time_difference_wad = time_difference * WAD; - - let shift_factor = pow(daily_price_shift_base, &time_difference_wad).unwrap_or(WAD); - let virtual_balance_overvalued = - mul_down_fixed(&virtual_balance_overvalued, &shift_factor).unwrap_or(U256::ZERO); + let shift_factor = pow(daily_price_shift_base, &time_difference_wad)?; + let virtual_balance_overvalued = mul_down_fixed(&virtual_balance_overvalued, &shift_factor)?; // Va = (Ra * (Vb + Rb)) / (((priceRatio - 1) * Vb) - Rb) - let price_ratio_minus_one = sqrt_price_ratio - WAD; - let denominator = mul_down_fixed(&price_ratio_minus_one, &virtual_balance_overvalued) - .unwrap_or(U256::ZERO) - - balances_scaled_overvalued; + let price_ratio_minus_one = sqrt_price_ratio + .checked_sub(WAD) + .ok_or(PoolError::MathOverflow)?; + let denominator = mul_down_fixed(&price_ratio_minus_one, &virtual_balance_overvalued)? + .checked_sub(balances_scaled_overvalued) + .ok_or(PoolError::MathOverflow)?; + if denominator.is_zero() { + return Err(PoolError::ZeroInvariant); + } let virtual_balance_undervalued = (balances_scaled_undervalued * (virtual_balance_overvalued + balances_scaled_overvalued)) / denominator; if is_pool_above_center { - (virtual_balance_undervalued, virtual_balance_overvalued) + Ok((virtual_balance_undervalued, virtual_balance_overvalued)) } else { - (virtual_balance_overvalued, virtual_balance_undervalued) + Ok((virtual_balance_overvalued, virtual_balance_undervalued)) } } @@ -213,11 +231,11 @@ fn compute_price_ratio( balances_scaled_18: &[U256], virtual_balance_a: &U256, virtual_balance_b: &U256, -) -> U256 { +) -> Result { let (min_price, max_price) = - compute_price_range(balances_scaled_18, virtual_balance_a, virtual_balance_b); + compute_price_range(balances_scaled_18, virtual_balance_a, virtual_balance_b)?; - div_up_fixed(&max_price, &min_price).unwrap_or(U256::ZERO) + div_up_fixed(&max_price, &min_price) } /// Compute price range @@ -225,13 +243,17 @@ fn compute_price_range( balances_scaled_18: &[U256], virtual_balance_a: &U256, virtual_balance_b: &U256, -) -> (U256, U256) { +) -> Result<(U256, U256), PoolError> { let invariant = compute_invariant( balances_scaled_18, virtual_balance_a, virtual_balance_b, Rounding::RoundDown, - ); + )?; + + if invariant.is_zero() { + return Err(PoolError::ZeroInvariant); + } // P_min(a) = Vb^2 / invariant let min_price = (virtual_balance_b * virtual_balance_b) / invariant; @@ -239,11 +261,10 @@ fn compute_price_range( // P_max(a) = invariant / Va^2 let max_price = div_down_fixed( &invariant, - &mul_down_fixed(virtual_balance_a, virtual_balance_a).unwrap_or(U256::ZERO), - ) - .unwrap_or(U256::ZERO); + &mul_down_fixed(virtual_balance_a, virtual_balance_a)?, + )?; - (min_price, max_price) + Ok((min_price, max_price)) } /// Compute fourth root price ratio @@ -253,29 +274,25 @@ fn compute_fourth_root_price_ratio( end_fourth_root_price_ratio: &U256, price_ratio_update_start_time: &U256, price_ratio_update_end_time: &U256, -) -> U256 { +) -> Result { if current_timestamp >= price_ratio_update_end_time { - return *end_fourth_root_price_ratio; + return Ok(*end_fourth_root_price_ratio); } else if current_timestamp <= price_ratio_update_start_time { - return *start_fourth_root_price_ratio; + return Ok(*start_fourth_root_price_ratio); } let exponent = div_down_fixed( &(current_timestamp - price_ratio_update_start_time), &(price_ratio_update_end_time - price_ratio_update_start_time), - ) - .unwrap_or(U256::ZERO); + )?; let current_fourth_root_price_ratio = mul_down_fixed( start_fourth_root_price_ratio, &pow( - &div_down_fixed(end_fourth_root_price_ratio, start_fourth_root_price_ratio) - .unwrap_or(U256::ZERO), + &div_down_fixed(end_fourth_root_price_ratio, start_fourth_root_price_ratio)?, &exponent, - ) - .unwrap_or(U256::ZERO), - ) - .unwrap_or(U256::ZERO); + )?, + )?; // Since we're rounding current fourth root price ratio down, we only need to check the lower boundary. let minimum_fourth_root_price_ratio = @@ -286,9 +303,9 @@ fn compute_fourth_root_price_ratio( }; if current_fourth_root_price_ratio > minimum_fourth_root_price_ratio { - current_fourth_root_price_ratio + Ok(current_fourth_root_price_ratio) } else { - minimum_fourth_root_price_ratio + Ok(minimum_fourth_root_price_ratio) } } @@ -297,12 +314,12 @@ fn compute_centeredness( balances_scaled_18: &[U256], virtual_balance_a: &U256, virtual_balance_b: &U256, -) -> (U256, bool) { +) -> Result<(U256, bool), PoolError> { if balances_scaled_18[A] == U256::ZERO { // Also return false if both are 0 to be consistent with the logic below. - return (U256::ZERO, false); + return Ok((U256::ZERO, false)); } else if balances_scaled_18[B] == U256::ZERO { - return (U256::ZERO, true); + return Ok((U256::ZERO, true)); } let numerator = balances_scaled_18[A] * virtual_balance_b; @@ -311,13 +328,11 @@ fn compute_centeredness( // The centeredness is defined between 0 and 1. If the numerator is greater than the denominator, // we compute the inverse ratio. if numerator <= denominator { - let pool_centeredness = div_down_fixed(&numerator, &denominator).unwrap_or(U256::ZERO); - let is_pool_above_center = false; - (pool_centeredness, is_pool_above_center) + let pool_centeredness = div_down_fixed(&numerator, &denominator)?; + Ok((pool_centeredness, false)) } else { - let pool_centeredness = div_down_fixed(&denominator, &numerator).unwrap_or(U256::ZERO); - let is_pool_above_center = true; - (pool_centeredness, is_pool_above_center) + let pool_centeredness = div_down_fixed(&denominator, &numerator)?; + Ok((pool_centeredness, true)) } } @@ -327,15 +342,13 @@ pub fn compute_invariant( virtual_balance_a: &U256, virtual_balance_b: &U256, rounding: Rounding, -) -> U256 { +) -> Result { let total_balance_a = balances_scaled_18[A] + virtual_balance_a; let total_balance_b = balances_scaled_18[B] + virtual_balance_b; match rounding { - Rounding::RoundDown => { - mul_down_fixed(&total_balance_a, &total_balance_b).unwrap_or(U256::ZERO) - } - Rounding::RoundUp => mul_up_fixed(&total_balance_a, &total_balance_b).unwrap_or(U256::ZERO), + Rounding::RoundDown => mul_down_fixed(&total_balance_a, &total_balance_b), + Rounding::RoundUp => mul_up_fixed(&total_balance_a, &total_balance_b), } } @@ -347,7 +360,7 @@ pub fn compute_out_given_in( token_in_index: usize, token_out_index: usize, amount_given_scaled_18: &U256, -) -> Result { +) -> Result { let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 { (virtual_balance_a, virtual_balance_b) } else { @@ -360,26 +373,25 @@ pub fn compute_out_given_in( virtual_balance_a, virtual_balance_b, Rounding::RoundUp, - ); + )?; // Total (virtual + real) token out amount that should stay in the pool after the swap let new_total_token_out_pool_balance = div_up_fixed( &invariant, &(balances_scaled_18[token_in_index] + virtual_balance_token_in + amount_given_scaled_18), - ) - .unwrap_or(U256::ZERO); + )?; let current_total_token_out_pool_balance = balances_scaled_18[token_out_index] + virtual_balance_token_out; if new_total_token_out_pool_balance > current_total_token_out_pool_balance { - return Err("reClammMath: NegativeAmountOut".to_string()); + return Err(PoolError::ReClammNegativeAmountOut); } let amount_out_scaled_18 = current_total_token_out_pool_balance - new_total_token_out_pool_balance; if amount_out_scaled_18 > balances_scaled_18[token_out_index] { - return Err("reClammMath: AmountOutGreaterThanBalance".to_string()); + return Err(PoolError::TokenAmountOutIsGreaterThanBalance); } Ok(amount_out_scaled_18) @@ -393,9 +405,9 @@ pub fn compute_in_given_out( token_in_index: usize, token_out_index: usize, amount_out_scaled_18: &U256, -) -> Result { +) -> Result { if amount_out_scaled_18 > &balances_scaled_18[token_out_index] { - return Err("reClammMath: AmountOutGreaterThanBalance".to_string()); + return Err(PoolError::TokenAmountOutIsGreaterThanBalance); } // Round up, so the swapper absorbs any imprecision due to rounding @@ -404,7 +416,7 @@ pub fn compute_in_given_out( virtual_balance_a, virtual_balance_b, Rounding::RoundUp, - ); + )?; let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 { (virtual_balance_a, virtual_balance_b) @@ -416,10 +428,150 @@ pub fn compute_in_given_out( let amount_in_scaled_18 = div_up_fixed( &invariant, &(balances_scaled_18[token_out_index] + virtual_balance_token_out - amount_out_scaled_18), - ) - .unwrap_or(U256::ZERO) - - balances_scaled_18[token_in_index] + )? - balances_scaled_18[token_in_index] - virtual_balance_token_in; Ok(amount_in_scaled_18) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Price-range-update path: timestamps differ, price-ratio update inactive. + fn call_updating_price_range( + balances: [U256; 2], + last_virtual_a: U256, + last_virtual_b: U256, + ) -> Result<(U256, U256, bool), PoolError> { + let current_timestamp = U256::from(200u64); + let last_timestamp = U256::from(100u64); + let centeredness_margin = WAD / U256::from(2u64); + // Inactive when end_time is 0 (last_timestamp < end is false). + let price_ratio_update_start_time = U256::ZERO; + let price_ratio_update_end_time = U256::ZERO; + let fourth_root = WAD; + let daily_price_shift_base = WAD; + + compute_current_virtual_balances( + ¤t_timestamp, + &balances, + &last_virtual_a, + &last_virtual_b, + &daily_price_shift_base, + &last_timestamp, + ¢eredness_margin, + &fourth_root, + &fourth_root, + &price_ratio_update_start_time, + &price_ratio_update_end_time, + ) + } + + /// Price-ratio-update path: current time inside the update window. + fn call_updating_price_ratio( + balances: [U256; 2], + last_virtual_a: U256, + last_virtual_b: U256, + fourth_root_price_ratio: U256, + ) -> Result<(U256, U256, bool), PoolError> { + let last_timestamp = U256::from(100u64); + let current_timestamp = U256::from(150u64); + let price_ratio_update_start_time = U256::from(50u64); + let price_ratio_update_end_time = U256::from(200u64); + // Centeredness above margin so only the price-ratio branch runs first. + let centeredness_margin = WAD / U256::from(2u64); + let daily_price_shift_base = WAD; + + compute_current_virtual_balances( + ¤t_timestamp, + &balances, + &last_virtual_a, + &last_virtual_b, + &daily_price_shift_base, + &last_timestamp, + ¢eredness_margin, + &fourth_root_price_ratio, + &fourth_root_price_ratio, + &price_ratio_update_start_time, + &price_ratio_update_end_time, + ) + } + + #[test] + fn virtual_balances_err_zero_invariant_skewed_a() { + let result = call_updating_price_range( + [U256::from(100_000_000u64), U256::from(1u64)], + U256::from(100_000_000u64), + U256::from(100_000_000u64), + ); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn virtual_balances_err_zero_invariant_skewed_b() { + let result = call_updating_price_range( + [U256::from(1u64), U256::from(100_000_000u64)], + U256::from(100_000_000u64), + U256::from(100_000_000u64), + ); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn virtual_balances_err_zero_invariant_mild_skew() { + let result = call_updating_price_range( + [U256::from(1_000_000u64), U256::from(1u64)], + U256::from(1_000_000u64), + U256::from(1_000_000u64), + ); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn virtual_balances_control_unit_balances_ok() { + let result = call_updating_price_range( + [U256::from(1u64), U256::from(1u64)], + U256::from(1u64), + U256::from(1u64), + ); + assert!(result.is_ok()); + } + + #[test] + fn virtual_balances_err_price_ratio_update_when_sqrt_equals_wad() { + // fourth_root = WAD ⇒ sqrt_price_ratio = WAD ⇒ (sqrt_price_ratio - WAD) = 0. + let result = call_updating_price_ratio([WAD, WAD], WAD, WAD, WAD); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn compute_centeredness_ok_when_balances_nonzero() { + let balances = [WAD, WAD]; + let result = compute_centeredness(&balances, &WAD, &WAD); + assert!(result.is_ok()); + let (centeredness, _) = result.unwrap(); + assert_eq!(centeredness, WAD); + } + + #[test] + fn compute_fourth_root_price_ratio_mid_window_ok() { + let start = WAD; + let end = WAD; + let result = compute_fourth_root_price_ratio( + &U256::from(100u64), + &start, + &end, + &U256::from(50u64), + &U256::from(200u64), + ); + assert!(result.is_ok()); + } + + #[test] + fn compute_invariant_propagates_overflow() { + let balances = [U256::MAX, U256::MAX]; + let result = compute_invariant(&balances, &U256::MAX, &U256::MAX, Rounding::RoundDown); + assert!(matches!(result, Err(PoolError::MathOverflow))); + } +} diff --git a/rust/src/pools/reclamm/reclamm_pool.rs b/rust/src/pools/reclamm/reclamm_pool.rs index 58a5957..f75fed6 100644 --- a/rust/src/pools/reclamm/reclamm_pool.rs +++ b/rust/src/pools/reclamm/reclamm_pool.rs @@ -21,7 +21,10 @@ impl ReClammPool { } /// Compute current virtual balances - fn _compute_current_virtual_balances(&self, balances_scaled_18: &[U256]) -> (U256, U256, bool) { + fn _compute_current_virtual_balances( + &self, + balances_scaled_18: &[U256], + ) -> Result<(U256, U256, bool), PoolError> { compute_current_virtual_balances( &self.re_clamm_state.mutable.current_timestamp, balances_scaled_18, @@ -53,7 +56,7 @@ impl PoolBase for ReClammPool { fn on_swap(&self, swap_params: &SwapParams) -> Result { let compute_result = - self._compute_current_virtual_balances(&swap_params.balances_live_scaled_18); + self._compute_current_virtual_balances(&swap_params.balances_live_scaled_18)?; match swap_params.swap_kind { SwapKind::GivenIn => { @@ -64,8 +67,7 @@ impl PoolBase for ReClammPool { swap_params.token_in_index, swap_params.token_out_index, &swap_params.amount_scaled_18, - ) - .map_err(|_| PoolError::InvalidSwapParameters)?; + )?; Ok(amount_calculated_scaled_18) } @@ -77,8 +79,7 @@ impl PoolBase for ReClammPool { swap_params.token_in_index, swap_params.token_out_index, &swap_params.amount_scaled_18, - ) - .map_err(|_| PoolError::InvalidSwapParameters)?; + )?; Ok(amount_calculated_scaled_18) } diff --git a/rust/src/pools/reclamm/reclamm_pricing.rs b/rust/src/pools/reclamm/reclamm_pricing.rs index 4b013bf..089ab91 100644 --- a/rust/src/pools/reclamm/reclamm_pricing.rs +++ b/rust/src/pools/reclamm/reclamm_pricing.rs @@ -1,3 +1,4 @@ +use crate::common::errors::PoolError; use alloy_primitives::U256; /// Result struct for swap to target price calculation @@ -67,13 +68,15 @@ pub fn swap_reclamm_to_price( _protocol_fee_percentage: &U256, _pool_creator_fee_percentage: &U256, target_price_scaled_18: &U256, -) -> Result { +) -> Result { // Input validation if balances_live_scaled_18.len() != 2 || current_virtual_balances.len() != 2 || token_rates.len() != 2 { - return Err("Invalid input: arrays must have length 2".to_string()); + return Err(PoolError::InvalidInput( + "arrays must have length 2".to_string(), + )); } // Convert U256 inputs to f64 @@ -92,13 +95,15 @@ pub fn swap_reclamm_to_price( // Validate non-zero values if balance_a + virtual_a == 0.0 || balance_b + virtual_b == 0.0 { - return Err("Invalid pool state: zero total balance".to_string()); + return Err(PoolError::InvalidInput("zero total balance".to_string())); } if rate_a == 0.0 || rate_b == 0.0 { - return Err("Invalid rates: zero rate".to_string()); + return Err(PoolError::InvalidInput("zero rate".to_string())); } if target_price <= 0.0 { - return Err("Invalid target price: must be positive".to_string()); + return Err(PoolError::InvalidInput( + "target price must be positive".to_string(), + )); } // Calculate invariant and prices @@ -119,10 +124,10 @@ pub fn swap_reclamm_to_price( // Validate amounts if amount_out_scaled < 0.0 || amount_in_scaled < 0.0 { - return Err("Invalid calculation: negative amounts".to_string()); + return Err(PoolError::InvalidInput("negative amounts".to_string())); } if amount_out_scaled > balance_a { - return Err("Invalid calculation: amount out exceeds balance".to_string()); + return Err(PoolError::TokenAmountOutIsGreaterThanBalance); } Ok(SwapToTargetPriceResult { @@ -146,10 +151,10 @@ pub fn swap_reclamm_to_price( // Validate amounts if amount_out_scaled < 0.0 || amount_in_scaled < 0.0 { - return Err("Invalid calculation: negative amounts".to_string()); + return Err(PoolError::InvalidInput("negative amounts".to_string())); } if amount_out_scaled > balance_b { - return Err("Invalid calculation: amount out exceeds balance".to_string()); + return Err(PoolError::TokenAmountOutIsGreaterThanBalance); } Ok(SwapToTargetPriceResult { diff --git a/rust/src/pools/reclammv2/reclammv2_math.rs b/rust/src/pools/reclammv2/reclammv2_math.rs index e467f9c..271ebba 100644 --- a/rust/src/pools/reclammv2/reclammv2_math.rs +++ b/rust/src/pools/reclammv2/reclammv2_math.rs @@ -1,4 +1,5 @@ use crate::common::constants::{RAY, TWO_WAD, WAD}; +use crate::common::errors::PoolError; use crate::common::log_exp_math; use crate::common::maths::{ div_down_fixed, div_up_fixed, mul_div_up_fixed, mul_down_fixed, mul_up_fixed, pow_down_fixed, @@ -26,9 +27,9 @@ pub fn compute_current_virtual_balances( end_fourth_root_price_ratio: &U256, price_ratio_update_start_time: &U256, price_ratio_update_end_time: &U256, -) -> (U256, U256, bool) { +) -> Result<(U256, U256, bool), PoolError> { if last_timestamp == current_timestamp { - return (*last_virtual_balance_a, *last_virtual_balance_b, false); + return Ok((*last_virtual_balance_a, *last_virtual_balance_b, false)); } let mut current_virtual_balance_a = *last_virtual_balance_a; @@ -40,7 +41,7 @@ pub fn compute_current_virtual_balances( end_fourth_root_price_ratio, price_ratio_update_start_time, price_ratio_update_end_time, - ); + )?; let mut changed = false; @@ -54,7 +55,7 @@ pub fn compute_current_virtual_balances( balances_scaled_18, last_virtual_balance_a, last_virtual_balance_b, - ); + )?; current_virtual_balance_a = new_virtual_balance_a; current_virtual_balance_b = new_virtual_balance_b; @@ -65,7 +66,7 @@ pub fn compute_current_virtual_balances( balances_scaled_18, ¤t_virtual_balance_a, ¤t_virtual_balance_b, - ); + )?; // If the pool is outside the target range, track the market price by moving the price interval. if centeredness < *centeredness_margin { @@ -78,18 +79,18 @@ pub fn compute_current_virtual_balances( daily_price_shift_base, current_timestamp, last_timestamp, - ); + )?; current_virtual_balance_a = new_virtual_balance_a; current_virtual_balance_b = new_virtual_balance_b; changed = true; } - ( + Ok(( current_virtual_balance_a, current_virtual_balance_b, changed, - ) + )) } /// Compute virtual balances when updating price ratio @@ -98,13 +99,13 @@ fn compute_virtual_balances_updating_price_ratio( balances_scaled_18: &[U256], last_virtual_balance_a: &U256, last_virtual_balance_b: &U256, -) -> (U256, U256) { +) -> Result<(U256, U256), PoolError> { // Compute the current pool centeredness, which will remain constant. let (centeredness, is_pool_above_center) = compute_centeredness( balances_scaled_18, last_virtual_balance_a, last_virtual_balance_b, - ); + )?; // The overvalued token is the one with a lower token balance (therefore, rarer and more valuable). let ( @@ -136,26 +137,37 @@ fn compute_virtual_balances_updating_price_ratio( let sqrt_price_ratio = mul_down_fixed( current_fourth_root_price_ratio, current_fourth_root_price_ratio, - ) - .unwrap_or(U256::ZERO); + )?; // Using FixedPoint math as little as possible to improve the precision of the result. // Note: The input of sqrt must be a 36-decimal number, so that the final result is 18 decimals. - let sqrt_input = - centeredness * (centeredness + (U256::from(4) * sqrt_price_ratio) - TWO_WAD) + RAY; + let centeredness_inner = (centeredness + (U256::from(4) * sqrt_price_ratio)) + .checked_sub(TWO_WAD) + .ok_or(PoolError::MathOverflow)?; + let sqrt_input = centeredness * centeredness_inner + RAY; let sqrt_result = sqrt(&sqrt_input); - let virtual_balance_undervalued = balance_token_undervalued - * (WAD + centeredness + sqrt_result) - / (U256::from(2) * (sqrt_price_ratio - WAD)); + let q0_minus_one = sqrt_price_ratio + .checked_sub(WAD) + .ok_or(PoolError::MathOverflow)?; + let denominator = U256::from(2) * q0_minus_one; + if denominator.is_zero() { + return Err(PoolError::ZeroInvariant); + } + let virtual_balance_undervalued = + balance_token_undervalued * (WAD + centeredness + sqrt_result) / denominator; + + if last_virtual_balance_undervalued.is_zero() { + return Err(PoolError::ZeroInvariant); + } let virtual_balance_overvalued = virtual_balance_undervalued * last_virtual_balance_overvalued / last_virtual_balance_undervalued; if is_pool_above_center { - (virtual_balance_undervalued, virtual_balance_overvalued) + Ok((virtual_balance_undervalued, virtual_balance_overvalued)) } else { - (virtual_balance_overvalued, virtual_balance_undervalued) + Ok((virtual_balance_overvalued, virtual_balance_undervalued)) } } @@ -168,12 +180,20 @@ fn compute_virtual_balances_updating_price_range( daily_price_shift_base: &U256, current_timestamp: &U256, last_timestamp: &U256, -) -> (U256, U256) { +) -> Result<(U256, U256), PoolError> { + // Fail before price-ratio math when quoting a block older than the last update. + let duration = std::cmp::min( + current_timestamp + .checked_sub(*last_timestamp) + .ok_or(PoolError::TimestampBeforeLastUpdate)?, + THIRTY_DAYS_SECONDS, + ); + let sqrt_price_ratio = sqrt_scaled_18(&compute_price_ratio( balances_scaled_18, virtual_balance_a, virtual_balance_b, - )); + )?); // The overvalued token is the one with a lower token balance (therefore, rarer and more valuable). let ( @@ -219,36 +239,41 @@ fn compute_virtual_balances_updating_price_range( // | Qo = Square root of price ratio | // +-----------------------------------------+ - // Cap the duration (time between operations) at 30 days, to ensure `pow_down` does not overflow. - let duration = std::cmp::min(current_timestamp - last_timestamp, THIRTY_DAYS_SECONDS); - let mut virtual_balance_overvalued = mul_down_fixed( &virtual_balance_overvalued, - &pow_down_fixed(daily_price_shift_base, &(duration * WAD)).unwrap_or(WAD), - ) - .unwrap_or(U256::ZERO); + &pow_down_fixed(daily_price_shift_base, &(duration * WAD))?, + )?; // Ensure that Vo does not go below the minimum allowed value (corresponding to centeredness == 1). let min_virtual_balance_overvalued = div_down_fixed( &balances_scaled_overvalued, - &(sqrt_scaled_18(&sqrt_price_ratio) - WAD), - ) - .unwrap_or(U256::ZERO); + &sqrt_scaled_18(&sqrt_price_ratio) + .checked_sub(WAD) + .ok_or(PoolError::MathOverflow)?, + )?; if virtual_balance_overvalued < min_virtual_balance_overvalued { virtual_balance_overvalued = min_virtual_balance_overvalued; } + let q0_minus_one = sqrt_price_ratio + .checked_sub(WAD) + .ok_or(PoolError::MathOverflow)?; + let denominator = mul_down_fixed(&q0_minus_one, &virtual_balance_overvalued)? + .checked_sub(balances_scaled_overvalued) + .ok_or(PoolError::MathOverflow)?; + if denominator.is_zero() { + return Err(PoolError::ZeroInvariant); + } + let virtual_balance_undervalued = balances_scaled_undervalued * (virtual_balance_overvalued + balances_scaled_overvalued) - / (mul_down_fixed(&(sqrt_price_ratio - WAD), &virtual_balance_overvalued) - .unwrap_or(U256::ZERO) - - balances_scaled_overvalued); + / denominator; if is_pool_above_center { - (virtual_balance_undervalued, virtual_balance_overvalued) + Ok((virtual_balance_undervalued, virtual_balance_overvalued)) } else { - (virtual_balance_overvalued, virtual_balance_undervalued) + Ok((virtual_balance_overvalued, virtual_balance_undervalued)) } } @@ -257,11 +282,11 @@ fn compute_price_ratio( balances_scaled_18: &[U256], virtual_balance_a: &U256, virtual_balance_b: &U256, -) -> U256 { +) -> Result { let (min_price, max_price) = - compute_price_range(balances_scaled_18, virtual_balance_a, virtual_balance_b); + compute_price_range(balances_scaled_18, virtual_balance_a, virtual_balance_b)?; - div_up_fixed(&max_price, &min_price).unwrap_or(U256::ZERO) + div_up_fixed(&max_price, &min_price) } /// Compute price range @@ -269,13 +294,17 @@ fn compute_price_range( balances_scaled_18: &[U256], virtual_balance_a: &U256, virtual_balance_b: &U256, -) -> (U256, U256) { +) -> Result<(U256, U256), PoolError> { let current_invariant = compute_invariant( balances_scaled_18, virtual_balance_a, virtual_balance_b, Rounding::RoundDown, - ); + )?; + + if current_invariant.is_zero() { + return Err(PoolError::ZeroInvariant); + } // P_min(a) = Vb / (Va + Ra_max) // We don't have Ra_max, but: invariant=(Ra_max + Va)(Vb) @@ -289,11 +318,10 @@ fn compute_price_range( // P_max(a) = invariant / Va^2 let max_price = div_down_fixed( ¤t_invariant, - &mul_down_fixed(virtual_balance_a, virtual_balance_a).unwrap_or(U256::ZERO), - ) - .unwrap_or(U256::ZERO); + &mul_down_fixed(virtual_balance_a, virtual_balance_a)?, + )?; - (min_price, max_price) + Ok((min_price, max_price)) } /// Compute fourth root price ratio @@ -303,37 +331,33 @@ fn compute_fourth_root_price_ratio( end_fourth_root_price_ratio: &U256, price_ratio_update_start_time: &U256, price_ratio_update_end_time: &U256, -) -> U256 { +) -> Result { // if start and end time are the same, return end value. if current_time >= price_ratio_update_end_time { - *end_fourth_root_price_ratio + Ok(*end_fourth_root_price_ratio) } else if current_time <= price_ratio_update_start_time { - *start_fourth_root_price_ratio + Ok(*start_fourth_root_price_ratio) } else { let exponent = div_down_fixed( &(current_time - price_ratio_update_start_time), &(price_ratio_update_end_time - price_ratio_update_start_time), - ) - .unwrap_or(U256::ZERO); + )?; let current_fourth_root_price_ratio = mul_down_fixed( start_fourth_root_price_ratio, &log_exp_math::pow( - &div_down_fixed(end_fourth_root_price_ratio, start_fourth_root_price_ratio) - .unwrap_or(U256::ZERO), + &div_down_fixed(end_fourth_root_price_ratio, start_fourth_root_price_ratio)?, &exponent, - ) - .unwrap_or(WAD), - ) - .unwrap_or(U256::ZERO); + )?, + )?; // Since we're rounding current fourth root price ratio down, we only need to check the lower boundary. let minimum_fourth_root_price_ratio = std::cmp::min(start_fourth_root_price_ratio, end_fourth_root_price_ratio); - std::cmp::max( + Ok(std::cmp::max( *minimum_fourth_root_price_ratio, current_fourth_root_price_ratio, - ) + )) } } @@ -342,12 +366,12 @@ fn compute_centeredness( balances_scaled_18: &[U256], virtual_balance_a: &U256, virtual_balance_b: &U256, -) -> (U256, bool) { +) -> Result<(U256, bool), PoolError> { if balances_scaled_18[A].is_zero() { // Also return false if both are 0 to be consistent with the logic below. - return (U256::ZERO, false); + return Ok((U256::ZERO, false)); } else if balances_scaled_18[B].is_zero() { - return (U256::ZERO, true); + return Ok((U256::ZERO, true)); } let numerator = balances_scaled_18[A] * virtual_balance_b; @@ -356,13 +380,11 @@ fn compute_centeredness( // The centeredness is defined between 0 and 1. If the numerator is greater than the denominator, we compute // the inverse ratio. if numerator <= denominator { - let pool_centeredness = div_down_fixed(&numerator, &denominator).unwrap_or(U256::ZERO); - let is_pool_above_center = false; - (pool_centeredness, is_pool_above_center) + let pool_centeredness = div_down_fixed(&numerator, &denominator)?; + Ok((pool_centeredness, false)) } else { - let pool_centeredness = div_down_fixed(&denominator, &numerator).unwrap_or(U256::ZERO); - let is_pool_above_center = true; - (pool_centeredness, is_pool_above_center) + let pool_centeredness = div_down_fixed(&denominator, &numerator)?; + Ok((pool_centeredness, true)) } } @@ -372,18 +394,16 @@ pub fn compute_invariant( virtual_balance_a: &U256, virtual_balance_b: &U256, rounding: Rounding, -) -> U256 { +) -> Result { match rounding { Rounding::RoundDown => mul_down_fixed( &(balances_scaled_18[A] + virtual_balance_a), &(balances_scaled_18[B] + virtual_balance_b), - ) - .unwrap_or(U256::ZERO), + ), Rounding::RoundUp => mul_up_fixed( &(balances_scaled_18[A] + virtual_balance_a), &(balances_scaled_18[B] + virtual_balance_b), - ) - .unwrap_or(U256::ZERO), + ), } } @@ -395,7 +415,7 @@ pub fn compute_out_given_in( token_in_index: usize, token_out_index: usize, amount_in_scaled_18: &U256, -) -> Result { +) -> Result { let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 { (virtual_balance_a, virtual_balance_b) } else { @@ -409,7 +429,7 @@ pub fn compute_out_given_in( if amount_out_scaled_18 > balances_scaled_18[token_out_index] { // Amount out cannot be greater than the real balance of the token in the pool. - return Err("reClammMath: AmountOutGreaterThanBalance".to_string()); + return Err(PoolError::TokenAmountOutIsGreaterThanBalance); } Ok(amount_out_scaled_18) @@ -423,10 +443,10 @@ pub fn compute_in_given_out( token_in_index: usize, token_out_index: usize, amount_out_scaled_18: &U256, -) -> Result { +) -> Result { if amount_out_scaled_18 > &balances_scaled_18[token_out_index] { // Amount out cannot be greater than the real balance of the token in the pool. - return Err("reClammMath: AmountOutGreaterThanBalance".to_string()); + return Err(PoolError::TokenAmountOutIsGreaterThanBalance); } let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 { @@ -440,8 +460,7 @@ pub fn compute_in_given_out( &(balances_scaled_18[token_in_index] + virtual_balance_token_in), amount_out_scaled_18, &(balances_scaled_18[token_out_index] + virtual_balance_token_out - amount_out_scaled_18), - ) - .unwrap_or(U256::ZERO); + )?; Ok(amount_in_scaled_18) } @@ -450,3 +469,182 @@ pub fn compute_in_given_out( fn sqrt_scaled_18(value_scaled_18: &U256) -> U256 { sqrt(&(value_scaled_18 * WAD)) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Price-range-update path: timestamps differ, price-ratio update inactive. + fn call_updating_price_range( + balances: [U256; 2], + last_virtual_a: U256, + last_virtual_b: U256, + ) -> Result<(U256, U256, bool), PoolError> { + let current_timestamp = U256::from(200u64); + let last_timestamp = U256::from(100u64); + let centeredness_margin = WAD / U256::from(2u64); + // Inactive when end_time is 0 (last_timestamp < end is false). + let price_ratio_update_start_time = U256::ZERO; + let price_ratio_update_end_time = U256::ZERO; + let fourth_root = WAD; + let daily_price_shift_base = WAD; + + compute_current_virtual_balances( + ¤t_timestamp, + &balances, + &last_virtual_a, + &last_virtual_b, + &daily_price_shift_base, + &last_timestamp, + ¢eredness_margin, + &fourth_root, + &fourth_root, + &price_ratio_update_start_time, + &price_ratio_update_end_time, + ) + } + + /// Price-ratio-update path: current time inside the update window. + fn call_updating_price_ratio( + balances: [U256; 2], + last_virtual_a: U256, + last_virtual_b: U256, + fourth_root_price_ratio: U256, + ) -> Result<(U256, U256, bool), PoolError> { + let last_timestamp = U256::from(100u64); + let current_timestamp = U256::from(150u64); + let price_ratio_update_start_time = U256::from(50u64); + let price_ratio_update_end_time = U256::from(200u64); + // Centeredness above margin so only the price-ratio branch runs first. + let centeredness_margin = WAD / U256::from(2u64); + let daily_price_shift_base = WAD; + + compute_current_virtual_balances( + ¤t_timestamp, + &balances, + &last_virtual_a, + &last_virtual_b, + &daily_price_shift_base, + &last_timestamp, + ¢eredness_margin, + &fourth_root_price_ratio, + &fourth_root_price_ratio, + &price_ratio_update_start_time, + &price_ratio_update_end_time, + ) + } + + #[test] + fn virtual_balances_err_zero_invariant_skewed_a() { + let result = call_updating_price_range( + [U256::from(100_000_000u64), U256::from(1u64)], + U256::from(100_000_000u64), + U256::from(100_000_000u64), + ); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn virtual_balances_err_zero_invariant_skewed_b() { + let result = call_updating_price_range( + [U256::from(1u64), U256::from(100_000_000u64)], + U256::from(100_000_000u64), + U256::from(100_000_000u64), + ); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn virtual_balances_err_zero_invariant_mild_skew() { + let result = call_updating_price_range( + [U256::from(1_000_000u64), U256::from(1u64)], + U256::from(1_000_000u64), + U256::from(1_000_000u64), + ); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn virtual_balances_control_unit_balances_ok() { + let result = call_updating_price_range( + [U256::from(1u64), U256::from(1u64)], + U256::from(1u64), + U256::from(1u64), + ); + assert!(result.is_ok()); + } + + #[test] + fn virtual_balances_err_price_ratio_update_when_sqrt_equals_wad() { + // fourth_root = WAD ⇒ sqrt_price_ratio = WAD ⇒ (sqrt_price_ratio - WAD) = 0. + let result = call_updating_price_ratio([WAD, WAD], WAD, WAD, WAD); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } + + #[test] + fn compute_centeredness_ok_when_balances_nonzero() { + let balances = [WAD, WAD]; + let result = compute_centeredness(&balances, &WAD, &WAD); + assert!(result.is_ok()); + let (centeredness, _) = result.unwrap(); + assert_eq!(centeredness, WAD); + } + + #[test] + fn compute_centeredness_zero_balance_a() { + let balances = [U256::ZERO, WAD]; + let result = compute_centeredness(&balances, &WAD, &WAD).unwrap(); + assert_eq!(result, (U256::ZERO, false)); + } + + #[test] + fn compute_fourth_root_price_ratio_at_end_returns_end() { + let start = WAD; + let end = WAD * U256::from(2u64); + let result = compute_fourth_root_price_ratio( + &U256::from(200u64), + &start, + &end, + &U256::from(50u64), + &U256::from(200u64), + ) + .unwrap(); + assert_eq!(result, end); + } + + #[test] + fn compute_fourth_root_price_ratio_before_start_returns_start() { + let start = WAD; + let end = WAD * U256::from(2u64); + let result = compute_fourth_root_price_ratio( + &U256::from(10u64), + &start, + &end, + &U256::from(50u64), + &U256::from(200u64), + ) + .unwrap(); + assert_eq!(result, start); + } + + #[test] + fn compute_fourth_root_price_ratio_mid_window_ok() { + let start = WAD; + let end = WAD; // same start/end so pow base is 1 + let result = compute_fourth_root_price_ratio( + &U256::from(100u64), + &start, + &end, + &U256::from(50u64), + &U256::from(200u64), + ); + assert!(result.is_ok()); + } + + #[test] + fn compute_invariant_propagates_overflow() { + let balances = [U256::MAX, U256::MAX]; + let result = compute_invariant(&balances, &U256::MAX, &U256::MAX, Rounding::RoundDown); + assert!(matches!(result, Err(PoolError::MathOverflow))); + } +} diff --git a/rust/src/pools/reclammv2/reclammv2_pool.rs b/rust/src/pools/reclammv2/reclammv2_pool.rs index c5ffd40..3f8d0b1 100644 --- a/rust/src/pools/reclammv2/reclammv2_pool.rs +++ b/rust/src/pools/reclammv2/reclammv2_pool.rs @@ -21,7 +21,10 @@ impl ReClammV2Pool { } /// Compute current virtual balances for the pool - fn compute_current_virtual_balances(&self, balances_scaled_18: &[U256]) -> (U256, U256, bool) { + fn compute_current_virtual_balances( + &self, + balances_scaled_18: &[U256], + ) -> Result<(U256, U256, bool), PoolError> { compute_current_virtual_balances( &self.re_clamm_v2_state.mutable.current_timestamp, balances_scaled_18, @@ -53,7 +56,7 @@ impl PoolBase for ReClammV2Pool { fn on_swap(&self, swap_params: &SwapParams) -> Result { let compute_result = - self.compute_current_virtual_balances(&swap_params.balances_live_scaled_18); + self.compute_current_virtual_balances(&swap_params.balances_live_scaled_18)?; match swap_params.swap_kind { SwapKind::GivenIn => { diff --git a/rust/src/pools/stable/stable_math.rs b/rust/src/pools/stable/stable_math.rs index b966612..7298b8e 100644 --- a/rust/src/pools/stable/stable_math.rs +++ b/rust/src/pools/stable/stable_math.rs @@ -11,6 +11,11 @@ pub const _MIN_INVARIANT_RATIO: u64 = 60e16 as u64; // 60% pub const _MAX_INVARIANT_RATIO: u64 = 500e16 as u64; // 500% /// Calculate the invariant for the stable swap curve. +/// +/// Returns `Ok(0)` when every balance is zero (empty/uninitialized pool), matching the +/// Solidity/TS/Python reference. A single zero balance among non-zero others makes the +/// invariant undefined; those inputs return `Err(PoolError::StableZeroBalance)` instead of +/// panicking on divide-by-zero (Solidity would revert with `Panic(0x12)`). pub fn compute_invariant( amplification_parameter: &U256, balances: &[U256], @@ -23,6 +28,11 @@ pub fn compute_invariant( return Ok(U256::ZERO); } + // Any individual zero balance would divide by zero in the D_P product below. + if balances.iter().any(|b| b.is_zero()) { + return Err(PoolError::StableZeroBalance); + } + // Initial invariant and amplification let mut invariant = total_balance; let amp_times_total = amplification_parameter * U256::from(num_tokens); @@ -116,13 +126,23 @@ pub fn compute_in_given_exact_out( Ok(final_balance_in - balances_copy[token_index_in] + U256::ONE) } -/// Compute the balance of a token given the invariant +/// Compute the balance of a token given the invariant. +/// +/// Zero balances (or a zero invariant) make the stable balance equation undefined; return +/// a typed error rather than panicking on divide-by-zero. pub fn compute_balance( amplification_parameter: &U256, balances: &[U256], invariant: &U256, token_index: usize, ) -> Result { + if invariant.is_zero() { + return Err(PoolError::ZeroInvariant); + } + if balances.iter().any(|b| b.is_zero()) { + return Err(PoolError::StableZeroBalance); + } + let num_tokens = balances.len() as u64; let amp_times_total = amplification_parameter * U256::from(num_tokens); @@ -169,3 +189,49 @@ pub fn compute_balance( Err(PoolError::StableInvariantDidntConverge) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compute_invariant_all_zero_balances_returns_zero() { + let result = compute_invariant(&U256::from(1000u64), &[U256::ZERO, U256::ZERO]); + assert_eq!(result, Ok(U256::ZERO)); + } + + #[test] + fn compute_invariant_partial_zero_balance_errors() { + // 3-token pool with one drained balance — previously panic'd on / 0. + let balances = [ + U256::from(1_000_000u64), + U256::ZERO, + U256::from(1_000_000u64), + ]; + let result = compute_invariant(&U256::from(1000u64), &balances); + assert!(matches!(result, Err(PoolError::StableZeroBalance))); + } + + #[test] + fn compute_balance_zero_balance_errors() { + let balances = [ + U256::from(1_000_000u64), + U256::ZERO, + U256::from(1_000_000u64), + ]; + let result = compute_balance( + &U256::from(1000u64), + &balances, + &U256::from(2_000_000u64), + 0, + ); + assert!(matches!(result, Err(PoolError::StableZeroBalance))); + } + + #[test] + fn compute_balance_zero_invariant_errors() { + let balances = [U256::from(1_000_000u64), U256::from(1_000_000u64)]; + let result = compute_balance(&U256::from(1000u64), &balances, &U256::ZERO, 0); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); + } +} diff --git a/rust/src/vault/add_liquidity.rs b/rust/src/vault/add_liquidity.rs index 60e2d71..80543c5 100644 --- a/rust/src/vault/add_liquidity.rs +++ b/rust/src/vault/add_liquidity.rs @@ -3,7 +3,7 @@ use crate::common::errors::PoolError; use crate::common::pool_base::PoolBase; use crate::common::utils::{ - compute_and_charge_aggregate_swap_fees, copy_to_scaled18_apply_rate_round_down_array, + compute_and_charge_aggregate_swap_fees_raw, copy_to_scaled18_apply_rate_round_down_array, get_single_input_index, require_unbalanced_liquidity_enabled, to_raw_undo_rate_round_up, }; use crate::common::{to_scaled_18_apply_rate_round_down, types::*}; @@ -110,7 +110,7 @@ pub fn add_liquidity( // A Pool's token balance always decreases after an exit // Computes protocol and pool creator fee which is eventually taken from pool balance - let aggregate_swap_fee_amount_raw = compute_and_charge_aggregate_swap_fees( + let aggregate_swap_fee_amount_raw = compute_and_charge_aggregate_swap_fees_raw( &swap_fee_amounts_scaled18[i], &base_state.aggregate_swap_fee, &base_state.scaling_factors, diff --git a/rust/src/vault/remove_liquidity.rs b/rust/src/vault/remove_liquidity.rs index 9de2f63..8bfc16a 100644 --- a/rust/src/vault/remove_liquidity.rs +++ b/rust/src/vault/remove_liquidity.rs @@ -4,7 +4,7 @@ use crate::common::errors::PoolError; use crate::common::pool_base::PoolBase; use crate::common::types::*; use crate::common::utils::{ - compute_and_charge_aggregate_swap_fees, copy_to_scaled18_apply_rate_round_up_array, + compute_and_charge_aggregate_swap_fees_raw, copy_to_scaled18_apply_rate_round_up_array, get_single_input_index, require_unbalanced_liquidity_enabled, to_raw_undo_rate_round_down, }; use crate::hooks::types::HookState; @@ -143,7 +143,8 @@ pub fn remove_liquidity( // A Pool's token balance always decreases after an exit // Computes protocol and pool creator fee which is eventually taken from pool balance - let aggregate_swap_fee_amount_scaled18 = compute_and_charge_aggregate_swap_fees( + // Aggregate fee is in raw token units (not scaled-18) + let aggregate_swap_fee_amount_raw = compute_and_charge_aggregate_swap_fees_raw( &swap_fee_amounts_scaled18[i], &base_state.aggregate_swap_fee, &base_state.scaling_factors, @@ -152,7 +153,7 @@ pub fn remove_liquidity( )?; updated_balances_live_scaled18[i] -= - amounts_out_scaled18[i] + aggregate_swap_fee_amount_scaled18; + amounts_out_scaled18[i] + aggregate_swap_fee_amount_raw; } // Call after remove liquidity hook if needed diff --git a/rust/src/vault/swap.rs b/rust/src/vault/swap.rs index c9b7fee..931401c 100644 --- a/rust/src/vault/swap.rs +++ b/rust/src/vault/swap.rs @@ -6,7 +6,7 @@ use crate::common::maths::{complement_fixed, mul_div_up_fixed, mul_up_fixed}; use crate::common::pool_base::PoolBase; use crate::common::types::*; use crate::common::utils::{ - compute_and_charge_aggregate_swap_fees, find_case_insensitive_index_in_list, + compute_and_charge_aggregate_swap_fees_raw, find_case_insensitive_index_in_list, to_raw_undo_rate_round_down, to_raw_undo_rate_round_up, to_scaled_18_apply_rate_round_down, to_scaled_18_apply_rate_round_up, }; @@ -127,8 +127,8 @@ pub fn swap( } }; - // Compute and charge aggregate swap fees - let aggregate_swap_fee_amount_scaled_18 = compute_and_charge_aggregate_swap_fees( + // Compute and charge aggregate swap fees (returned in raw token units) + let aggregate_swap_fee_amount_raw = compute_and_charge_aggregate_swap_fees_raw( &total_swap_fee_amount_scaled_18, &base_state.aggregate_swap_fee, &base_state.scaling_factors, @@ -139,11 +139,11 @@ pub fn swap( // Update balances let (balance_in_increment, balance_out_decrement) = match swap_input.swap_kind { SwapKind::GivenIn => ( - amount_given_scaled_18 - aggregate_swap_fee_amount_scaled_18, + amount_given_scaled_18 - aggregate_swap_fee_amount_raw, amount_calculated_scaled_18, ), SwapKind::GivenOut => ( - amount_calculated_scaled_18 - aggregate_swap_fee_amount_scaled_18, + amount_calculated_scaled_18 - aggregate_swap_fee_amount_raw, amount_given_scaled_18, ), }; diff --git a/rust/tests/test_reclamm_panic_regression.rs b/rust/tests/test_reclamm_panic_regression.rs new file mode 100644 index 0000000..12195a9 --- /dev/null +++ b/rust/tests/test_reclamm_panic_regression.rs @@ -0,0 +1,121 @@ +//! Regression: reCLAMM on_swap must not panic on off-centre dusty pool states. + +use alloy_primitives::U256; +use balancer_maths_rust::common::types::{BasePoolState, SwapKind, SwapParams}; +use balancer_maths_rust::pools::reclamm::{ + ReClammImmutable, ReClammMutable, ReClammPool, ReClammState, +}; +use balancer_maths_rust::pools::reclammv2::{ + ReClammV2Immutable, ReClammV2Mutable, ReClammV2Pool, ReClammV2State, +}; +use balancer_maths_rust::{PoolBase, PoolError}; + +const WAD: U256 = U256::from_limbs([1_000_000_000_000_000_000, 0, 0, 0]); + +fn base_state(balances: Vec, pool_type: &str) -> BasePoolState { + BasePoolState { + pool_address: "0x0000000000000000000000000000000000000001".to_string(), + pool_type: pool_type.to_string(), + tokens: vec![ + "0x000000000000000000000000000000000000000a".to_string(), + "0x000000000000000000000000000000000000000b".to_string(), + ], + scaling_factors: vec![U256::from(1u64), U256::from(1u64)], + token_rates: vec![WAD, WAD], + balances_live_scaled_18: balances, + swap_fee: U256::ZERO, + aggregate_swap_fee: U256::ZERO, + total_supply: WAD, + supports_unbalanced_liquidity: false, + hook_type: None, + } +} + +/// Case 1 from findings: balances [1e8, 1], virtual [1e8, 1e8] - truncates invariant to 0. +fn panic_case_balances() -> Vec { + Vec::from([U256::from(100_000_000u64), U256::from(1u64)]) +} + +fn panic_case_virtuals() -> Vec { + let virtual_balance = U256::from(100_000_000u64); + Vec::from([virtual_balance, virtual_balance]) +} + +fn make_v2_pool() -> ReClammV2Pool { + let balances = panic_case_balances(); + let state = ReClammV2State { + base: base_state(balances.clone(), "RECLAMM_V2"), + mutable: ReClammV2Mutable { + last_virtual_balances: panic_case_virtuals(), + daily_price_shift_base: WAD, + last_timestamp: U256::from(100u64), + current_timestamp: U256::from(200u64), + centeredness_margin: WAD / U256::from(2u64), + start_fourth_root_price_ratio: WAD, + end_fourth_root_price_ratio: WAD, + // Price-ratio update inactive so the price-range path runs. + price_ratio_update_start_time: U256::ZERO, + price_ratio_update_end_time: U256::ZERO, + }, + immutable: ReClammV2Immutable { + pool_address: "0x0000000000000000000000000000000000000001".to_string(), + tokens: vec![ + "0x000000000000000000000000000000000000000a".to_string(), + "0x000000000000000000000000000000000000000b".to_string(), + ], + }, + }; + ReClammV2Pool::new(state) +} + +fn make_v1_pool() -> ReClammPool { + let balances = panic_case_balances(); + let state = ReClammState { + base: base_state(balances.clone(), "RECLAMM"), + mutable: ReClammMutable { + last_virtual_balances: panic_case_virtuals(), + daily_price_shift_base: WAD, + last_timestamp: U256::from(100u64), + current_timestamp: U256::from(200u64), + centeredness_margin: WAD / U256::from(2u64), + start_fourth_root_price_ratio: WAD, + end_fourth_root_price_ratio: WAD, + price_ratio_update_start_time: U256::ZERO, + price_ratio_update_end_time: U256::ZERO, + }, + immutable: ReClammImmutable { + pool_address: "0x0000000000000000000000000000000000000001".to_string(), + tokens: vec![ + "0x000000000000000000000000000000000000000a".to_string(), + "0x000000000000000000000000000000000000000b".to_string(), + ], + }, + }; + ReClammPool::new(state) +} + +fn swap_params(balances: Vec) -> SwapParams { + SwapParams { + swap_kind: SwapKind::GivenIn, + token_in_index: 0, + token_out_index: 1, + amount_scaled_18: U256::from(1u64), + balances_live_scaled_18: balances, + } +} + +#[test] +fn reclamm_v2_on_swap_returns_zero_invariant_on_dusty_off_centre_pool() { + let pool = make_v2_pool(); + let params = swap_params(panic_case_balances()); + let result = pool.on_swap(¶ms); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); +} + +#[test] +fn reclamm_v1_on_swap_returns_zero_invariant_on_dusty_off_centre_pool() { + let pool = make_v1_pool(); + let params = swap_params(panic_case_balances()); + let result = pool.on_swap(¶ms); + assert!(matches!(result, Err(PoolError::ZeroInvariant))); +} diff --git a/rust/tests/test_reclamm_swap_to_price.rs b/rust/tests/test_reclamm_swap_to_price.rs index 2a97c5f..1091020 100644 --- a/rust/tests/test_reclamm_swap_to_price.rs +++ b/rust/tests/test_reclamm_swap_to_price.rs @@ -7,6 +7,7 @@ use balancer_maths_rust::pools::reclamm::{ SwapToTargetPriceResult, }; use balancer_maths_rust::vault::Vault; +use balancer_maths_rust::PoolError; use std::str::FromStr; /// Single source of truth for test pool data @@ -314,11 +315,10 @@ fn test_amount_out_exceeds_balance_greater() { result ); - let error_msg = result.unwrap_err(); assert!( - error_msg.contains("amount out exceeds balance"), - "Expected 'amount out exceeds balance' error, but got: {}", - error_msg + matches!(result, Err(PoolError::TokenAmountOutIsGreaterThanBalance)), + "Expected TokenAmountOutIsGreaterThanBalance, but got: {:?}", + result ); } @@ -352,10 +352,9 @@ fn test_amount_out_exceeds_balance_less() { result ); - let error_msg = result.unwrap_err(); assert!( - error_msg.contains("amount out exceeds balance"), - "Expected 'amount out exceeds balance' error, but got: {}", - error_msg + matches!(result, Err(PoolError::TokenAmountOutIsGreaterThanBalance)), + "Expected TokenAmountOutIsGreaterThanBalance, but got: {:?}", + result ); } diff --git a/rust/tests/test_timestamp_before_last_update.rs b/rust/tests/test_timestamp_before_last_update.rs new file mode 100644 index 0000000..76760c3 --- /dev/null +++ b/rust/tests/test_timestamp_before_last_update.rs @@ -0,0 +1,143 @@ +//! Regression: quoting before a pool's last update must return PoolError, not panic. + +use alloy_primitives::{I256, U256}; +use balancer_maths_rust::common::types::{BasePoolState, SwapKind, SwapParams}; +use balancer_maths_rust::pools::quantamm::{ + QuantAmmImmutable, QuantAmmMutable, QuantAmmPool, QuantAmmState, +}; +use balancer_maths_rust::pools::reclamm::{ + ReClammImmutable, ReClammMutable, ReClammPool, ReClammState, +}; +use balancer_maths_rust::pools::reclammv2::{ + ReClammV2Immutable, ReClammV2Mutable, ReClammV2Pool, ReClammV2State, +}; +use balancer_maths_rust::{PoolBase, PoolError}; + +const WAD: U256 = U256::from_limbs([1_000_000_000_000_000_000, 0, 0, 0]); +const HALF_WAD: U256 = U256::from_limbs([500_000_000_000_000_000, 0, 0, 0]); + +fn base_state(balances: Vec, pool_type: &str) -> BasePoolState { + BasePoolState { + pool_address: "0x0000000000000000000000000000000000000001".to_string(), + pool_type: pool_type.to_string(), + tokens: vec![ + "0x000000000000000000000000000000000000000a".to_string(), + "0x000000000000000000000000000000000000000b".to_string(), + ], + scaling_factors: vec![U256::from(1u64), U256::from(1u64)], + token_rates: vec![WAD, WAD], + balances_live_scaled_18: balances, + swap_fee: U256::ZERO, + aggregate_swap_fee: U256::ZERO, + total_supply: WAD, + supports_unbalanced_liquidity: false, + hook_type: None, + } +} + +/// Off-centre but non-dusty: centeredness < margin, invariant non-zero so the +/// price-range path reaches the duration subtraction. +fn off_centre_balances() -> Vec { + Vec::from([WAD, WAD / U256::from(10u64)]) +} + +fn off_centre_virtuals() -> Vec { + Vec::from([WAD, WAD]) +} + +fn swap_params(balances: Vec) -> SwapParams { + SwapParams { + swap_kind: SwapKind::GivenIn, + token_in_index: 0, + token_out_index: 1, + amount_scaled_18: U256::from(1u64), + balances_live_scaled_18: balances, + } +} + +#[test] +fn quantamm_new_returns_error_when_timestamp_before_last_update() { + let balances = vec![WAD, WAD]; + // Packed [w0, w1, m0, m1] for a 2-token pool. + let weights_and_multipliers = vec![ + I256::from_raw(HALF_WAD), + I256::from_raw(HALF_WAD), + I256::ZERO, + I256::ZERO, + ]; + let state = QuantAmmState { + base: base_state(balances, "QUANT_AMM"), + mutable: QuantAmmMutable { + first_four_weights_and_multipliers: weights_and_multipliers, + second_four_weights_and_multipliers: vec![], + last_update_time: U256::from(200u64), + last_interop_time: U256::from(300u64), + // Quoting a block earlier than the last weight update. + current_timestamp: U256::from(100u64), + }, + immutable: QuantAmmImmutable { + max_trade_size_ratio: HALF_WAD, + }, + }; + + let result = QuantAmmPool::new(state); + assert!(matches!(result, Err(PoolError::TimestampBeforeLastUpdate))); +} + +#[test] +fn reclamm_v2_on_swap_returns_error_when_timestamp_before_last_update() { + let balances = off_centre_balances(); + let state = ReClammV2State { + base: base_state(balances.clone(), "RECLAMM_V2"), + mutable: ReClammV2Mutable { + last_virtual_balances: off_centre_virtuals(), + daily_price_shift_base: WAD, + last_timestamp: U256::from(200u64), + current_timestamp: U256::from(100u64), + centeredness_margin: WAD / U256::from(2u64), + start_fourth_root_price_ratio: WAD, + end_fourth_root_price_ratio: WAD, + price_ratio_update_start_time: U256::ZERO, + price_ratio_update_end_time: U256::ZERO, + }, + immutable: ReClammV2Immutable { + pool_address: "0x0000000000000000000000000000000000000001".to_string(), + tokens: vec![ + "0x000000000000000000000000000000000000000a".to_string(), + "0x000000000000000000000000000000000000000b".to_string(), + ], + }, + }; + let pool = ReClammV2Pool::new(state); + let result = pool.on_swap(&swap_params(balances)); + assert!(matches!(result, Err(PoolError::TimestampBeforeLastUpdate))); +} + +#[test] +fn reclamm_v1_on_swap_returns_error_when_timestamp_before_last_update() { + let balances = off_centre_balances(); + let state = ReClammState { + base: base_state(balances.clone(), "RECLAMM"), + mutable: ReClammMutable { + last_virtual_balances: off_centre_virtuals(), + daily_price_shift_base: WAD, + last_timestamp: U256::from(200u64), + current_timestamp: U256::from(100u64), + centeredness_margin: WAD / U256::from(2u64), + start_fourth_root_price_ratio: WAD, + end_fourth_root_price_ratio: WAD, + price_ratio_update_start_time: U256::ZERO, + price_ratio_update_end_time: U256::ZERO, + }, + immutable: ReClammImmutable { + pool_address: "0x0000000000000000000000000000000000000001".to_string(), + tokens: vec![ + "0x000000000000000000000000000000000000000a".to_string(), + "0x000000000000000000000000000000000000000b".to_string(), + ], + }, + }; + let pool = ReClammPool::new(state); + let result = pool.on_swap(&swap_params(balances)); + assert!(matches!(result, Err(PoolError::TimestampBeforeLastUpdate))); +}