diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index 2a23297..07c2f47 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - `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 diff --git a/rust/src/common/errors.rs b/rust/src/common/errors.rs index 5060c10..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 @@ -94,6 +95,24 @@ pub enum PoolError { /// 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 { @@ -147,20 +166,18 @@ impl fmt::Display for PoolError { 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/pools/buffer/buffer_math.rs b/rust/src/pools/buffer/buffer_math.rs index 9798d12..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,26 +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, + }); } _convert_to_shares(amount_raw, rate, Rounding::RoundDown) - .map_err(|e| e.to_string()) } 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), + }); } _convert_to_assets(amount_raw, rate, Rounding::RoundUp) - .map_err(|e| e.to_string()) } } } @@ -60,12 +58,10 @@ pub fn calculate_buffer_amounts( SwapKind::GivenIn => { // previewRedeem _convert_to_assets(amount_raw, rate, Rounding::RoundDown) - .map_err(|e| e.to_string()) } SwapKind::GivenOut => { // previewWithdraw _convert_to_shares(amount_raw, rate, Rounding::RoundUp) - .map_err(|e| e.to_string()) } } } @@ -73,11 +69,7 @@ pub fn calculate_buffer_amounts( } /// Convert assets to shares -fn _convert_to_shares( - assets: &U256, - rate: &U256, - rounding: Rounding, -) -> Result { +fn _convert_to_shares(assets: &U256, rate: &U256, rounding: Rounding) -> Result { match rounding { Rounding::RoundUp => div_up_fixed(assets, rate), Rounding::RoundDown => div_down_fixed(assets, rate), @@ -85,11 +77,7 @@ fn _convert_to_shares( } /// Convert shares to assets -fn _convert_to_assets( - shares: &U256, - rate: &U256, - rounding: Rounding, -) -> Result { +fn _convert_to_assets(shares: &U256, rate: &U256, rounding: Rounding) -> Result { match rounding { 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/reclamm/reclamm_math.rs b/rust/src/pools/reclamm/reclamm_math.rs index a6626f0..0adbe3f 100644 --- a/rust/src/pools/reclamm/reclamm_math.rs +++ b/rust/src/pools/reclamm/reclamm_math.rs @@ -360,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 { @@ -373,27 +373,25 @@ pub fn compute_out_given_in( virtual_balance_a, virtual_balance_b, Rounding::RoundUp, - ) - .map_err(|e| e.to_string())?; + )?; // 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), - ) - .map_err(|e| e.to_string())?; + )?; 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) @@ -407,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 @@ -418,8 +416,7 @@ pub fn compute_in_given_out( virtual_balance_a, virtual_balance_b, Rounding::RoundUp, - ) - .map_err(|e| e.to_string())?; + )?; let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 { (virtual_balance_a, virtual_balance_b) @@ -431,9 +428,7 @@ 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), - ) - .map_err(|e| e.to_string())? - - balances_scaled_18[token_in_index] + )? - balances_scaled_18[token_in_index] - virtual_balance_token_in; Ok(amount_in_scaled_18) diff --git a/rust/src/pools/reclamm/reclamm_pool.rs b/rust/src/pools/reclamm/reclamm_pool.rs index 043f454..f75fed6 100644 --- a/rust/src/pools/reclamm/reclamm_pool.rs +++ b/rust/src/pools/reclamm/reclamm_pool.rs @@ -67,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) } @@ -80,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 4931b25..271ebba 100644 --- a/rust/src/pools/reclammv2/reclammv2_math.rs +++ b/rust/src/pools/reclammv2/reclammv2_math.rs @@ -415,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 { @@ -429,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) @@ -443,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 { @@ -460,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), - ) - .map_err(|e| e.to_string())?; + )?; Ok(amount_in_scaled_18) } 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 ); }