Skip to content
Closed
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
5 changes: 5 additions & 0 deletions rust/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ All notable changes to this project will be documented in this file.

### 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`.
- 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`.

### 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.

## [0.4.1] - 2025-11-20

Expand Down
56 changes: 52 additions & 4 deletions rust/src/common/maths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use alloy_primitives::U256;

/// Multiply two U256s and round up
pub fn mul_up_fixed(a: &U256, b: &U256) -> Result<U256, PoolError> {
let product = a * b;
let product = a.checked_mul(*b).ok_or(PoolError::MathOverflow)?;
if product.is_zero() {
return Ok(U256::ZERO);
}
Expand All @@ -23,7 +23,7 @@ pub fn div_up_fixed(a: &U256, b: &U256) -> Result<U256, PoolError> {

/// Multiply two U256s and round down
pub fn mul_down_fixed(a: &U256, b: &U256) -> Result<U256, PoolError> {
let product = a * b;
let product = a.checked_mul(*b).ok_or(PoolError::MathOverflow)?;
let result = product / WAD;
Ok(result)
}
Expand All @@ -37,7 +37,7 @@ pub fn div_down_fixed(a: &U256, b: &U256) -> Result<U256, PoolError> {
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)
}
Expand All @@ -53,7 +53,7 @@ pub fn div_up(a: &U256, b: &U256) -> Result<U256, PoolError> {

/// Multiply and divide with up rounding
pub fn mul_div_up_fixed(a: &U256, b: &U256, c: &U256) -> Result<U256, PoolError> {
let product = a * b;
let product = a.checked_mul(*b).ok_or(PoolError::MathOverflow)?;
if product.is_zero() {
return Ok(U256::ZERO);
}
Expand Down Expand Up @@ -129,3 +129,51 @@ pub fn complement_fixed(x: &U256) -> Result<U256, PoolError> {
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);
}
}
2 changes: 1 addition & 1 deletion rust/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions rust/src/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
46 changes: 34 additions & 12 deletions rust/src/hooks/akron/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions rust/src/hooks/exit_fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 22 additions & 10 deletions rust/src/pools/buffer/buffer_math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ pub fn calculate_buffer_amounts(
amount_raw, max_assets
));
}
Ok(_convert_to_shares(amount_raw, rate, Rounding::RoundDown))
_convert_to_shares(amount_raw, rate, Rounding::RoundDown)
.map_err(|e| e.to_string())
}
SwapKind::GivenOut => {
// previewMint
Expand All @@ -48,7 +49,8 @@ pub fn calculate_buffer_amounts(
max_mint.unwrap_or(&U256::ZERO)
));
}
Ok(_convert_to_assets(amount_raw, rate, Rounding::RoundUp))
_convert_to_assets(amount_raw, rate, Rounding::RoundUp)
.map_err(|e| e.to_string())
}
}
}
Expand All @@ -57,29 +59,39 @@ 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)
.map_err(|e| e.to_string())
}
SwapKind::GivenOut => {
// previewWithdraw
Ok(_convert_to_shares(amount_raw, rate, Rounding::RoundUp))
_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) -> U256 {
fn _convert_to_shares(
assets: &U256,
rate: &U256,
rounding: Rounding,
) -> Result<U256, crate::common::errors::PoolError> {
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<U256, crate::common::errors::PoolError> {
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),
}
}
Loading
Loading