Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>` / `From<&str>` impls for `PoolError`.

## [0.4.1] - 2025-11-20

Expand Down
41 changes: 29 additions & 12 deletions rust/src/common/errors.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String> 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())
}
}
36 changes: 12 additions & 24 deletions rust/src/pools/buffer/buffer_math.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,7 +23,7 @@ pub fn calculate_buffer_amounts(
rate: &U256,
max_deposit: Option<&U256>,
max_mint: Option<&U256>,
) -> Result<U256, String> {
) -> Result<U256, PoolError> {
match direction {
WrappingDirection::Wrap => {
// Amount in is underlying tokens, amount out is wrapped tokens
Expand All @@ -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())
}
}
}
Expand All @@ -60,36 +58,26 @@ 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())
}
}
}
}
}

/// Convert assets to shares
fn _convert_to_shares(
assets: &U256,
rate: &U256,
rounding: Rounding,
) -> Result<U256, crate::common::errors::PoolError> {
fn _convert_to_shares(assets: &U256, rate: &U256, rounding: Rounding) -> Result<U256, PoolError> {
match rounding {
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,
) -> Result<U256, crate::common::errors::PoolError> {
fn _convert_to_assets(shares: &U256, rate: &U256, rounding: Rounding) -> Result<U256, PoolError> {
match rounding {
Rounding::RoundUp => mul_up_fixed(shares, rate),
Rounding::RoundDown => mul_down_fixed(shares, rate),
Expand Down
5 changes: 3 additions & 2 deletions rust/src/pools/buffer/erc4626_buffer_wrap_or_unwrap.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<U256, String> {
) -> Result<U256, PoolError> {
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
Expand Down
23 changes: 9 additions & 14 deletions rust/src/pools/reclamm/reclamm_math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ pub fn compute_out_given_in(
token_in_index: usize,
token_out_index: usize,
amount_given_scaled_18: &U256,
) -> Result<U256, String> {
) -> Result<U256, PoolError> {
let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 {
(virtual_balance_a, virtual_balance_b)
} else {
Expand All @@ -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)
Expand All @@ -407,9 +405,9 @@ pub fn compute_in_given_out(
token_in_index: usize,
token_out_index: usize,
amount_out_scaled_18: &U256,
) -> Result<U256, String> {
) -> Result<U256, PoolError> {
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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions rust/src/pools/reclamm/reclamm_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
23 changes: 14 additions & 9 deletions rust/src/pools/reclamm/reclamm_pricing.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::common::errors::PoolError;
use alloy_primitives::U256;

/// Result struct for swap to target price calculation
Expand Down Expand Up @@ -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<SwapToTargetPriceResult, String> {
) -> Result<SwapToTargetPriceResult, PoolError> {
// 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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
11 changes: 5 additions & 6 deletions rust/src/pools/reclammv2/reclammv2_math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ pub fn compute_out_given_in(
token_in_index: usize,
token_out_index: usize,
amount_in_scaled_18: &U256,
) -> Result<U256, String> {
) -> Result<U256, PoolError> {
let (virtual_balance_token_in, virtual_balance_token_out) = if token_in_index == 0 {
(virtual_balance_a, virtual_balance_b)
} else {
Expand All @@ -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)
Expand All @@ -443,10 +443,10 @@ pub fn compute_in_given_out(
token_in_index: usize,
token_out_index: usize,
amount_out_scaled_18: &U256,
) -> Result<U256, String> {
) -> Result<U256, PoolError> {
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 {
Expand All @@ -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)
}
Expand Down
Loading
Loading