Skip to content

Rust fix 1 - #166

Merged
johngrantuk merged 13 commits into
mainfrom
rust-fix-1
Aug 12, 2026
Merged

Rust fix 1#166
johngrantuk merged 13 commits into
mainfrom
rust-fix-1

Conversation

@johngrantuk

@johngrantuk johngrantuk commented Aug 11, 2026

Copy link
Copy Markdown
Member

Report issue:

1. reCLAMM: unchecked arithmetic panics on reachable pool states

This is the one we would most like fixed. It panics rather than returning PoolError, so a
single pathological pool takes down the calling thread, and it is on the ordinary quoting path
(ReClammV2Pool::on_swap), not only on an edge API.

Deterministic reproduction

Calling the public compute_current_virtual_balances with these arguments panics with
attempt to divide by zero at ruint-1.20.0/src/div.rs:93:

use balancer_maths_rust::pools::reclammv2::compute_current_virtual_balances;
use alloy_primitives::U256;

const WAD: u64 = 1_000_000_000_000_000_000;

// Any of these three panic. The fourth (all balances = 1) does not.
let cases = [
    (vec![U256::from(100_000_000u64), U256::from(1u64)], 100_000_000u64, 100_000_000u64),
    (vec![U256::from(1u64), U256::from(100_000_000u64)], 100_000_000u64, 100_000_000u64),
    (vec![U256::from(1_000_000u64),   U256::from(1u64)],   1_000_000u64,   1_000_000u64),
];

for (balances, va, vb) in cases {
    compute_current_virtual_balances(
        &U256::from(1_000_000u64),          // current_timestamp
        &balances,
        &U256::from(va),                    // last_virtual_balance_a
        &U256::from(vb),                    // last_virtual_balance_b
        &U256::from(WAD - 1),               // daily_price_shift_base
        &U256::from(999_000u64),            // last_timestamp
        &U256::from(WAD / 2),               // centeredness_margin — forces the range update
        &U256::from(WAD),                   // start_fourth_root_price_ratio
        &U256::from(WAD),                   // end_fourth_root_price_ratio
        &U256::ZERO, &U256::ZERO,
    );
}

Two conditions have to coincide, and both are ordinary:

  1. the pool is off-centre enough that centeredness < centeredness_margin, so
    compute_virtual_balances_updating_price_range runs, and
  2. (balance_a + virtual_a) * (balance_b + virtual_b) < 1e18, so compute_invariant truncates the
    product to zero.

Condition 2 needs no overflow — mul_down_fixed is a * b / WAD, so any product under WAD
truncates. A pool whose scaled-18 balances and virtual balances are small enough qualifies. The path
is then:

compute_current_virtual_balances
  └ compute_virtual_balances_updating_price_range
      └ compute_price_ratio
          └ compute_price_range
              └ virtual_balance_b * virtual_balance_b / current_invariant   // current_invariant == 0

Observed in production

57–69 panics per run of our mainnet integration test, RUST_BACKTRACE=1:

panicked at ruint-1.20.0/src/div.rs:93:9: attempt to divide by zero
   3: <ruint::Uint<256, 4>>::div_rem_by_ref
   4: <ruint::Uint<256, 4>>::wrapping_div
   5: balancer_maths_rust::pools::reclammv2::reclammv2_math::compute_price_range
   6: balancer_maths_rust::pools::reclammv2::reclammv2_math::compute_price_ratio
   7: balancer_maths_rust::pools::reclammv2::reclammv2_math::compute_virtual_balances_updating_price_range
   8: balancer_maths_rust::pools::reclammv2::reclammv2_math::compute_current_virtual_balances

The division above is not the only unguarded spot

Auditing reclammv2_math.rs for raw / and - on U256 turns up six more, all reachable from the
same public entry point. Line numbers are from the published 0.4.4 source:

Line Expression Fails when
145 centeredness + (4 * sqrt_price_ratio) - TWO_WAD sqrt_price_ratio truncated to 0 ⇒ underflow
150 / (2 * (sqrt_price_ratio - WAD)) sqrt_price_ratio < WAD ⇒ underflow; == WAD ⇒ divide by zero
153 / last_virtual_balance_undervalued that virtual balance is 0
223 current_timestamp - last_timestamp quoting a block older than the pool's last update
234 sqrt_scaled_18(&sqrt_price_ratio) - WAD ratio below 1 ⇒ underflow
244–246 / (mul_down_fixed(sqrt_price_ratio - WAD, v_over) - balances_over) underflow, or equality ⇒ divide by zero
284 / current_invariant the reproduction above

pools/reclamm/reclamm_math.rs (the v1 module) has the same shape.

Suggested fix: make compute_price_range, compute_price_ratio and
compute_virtual_balances_updating_price_range return Result<_, PoolError> and use checked
operations, so callers get an error they can skip the pool on rather than an unwinding panic.

@zach030 zach030 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! lgtm ✅

rust: Align remaining Result<_, String> APIs to PoolError
rust: No panic when current time < last for quant and reclamm
Fix `stable_math::compute_invariant` panics on a zero balance
rust: Renamed  →  to make clear it returns the fee in the token's raw…
fix(rust): propagate math errors instead of masking with unwrap_or(ZERO)
@johngrantuk
johngrantuk merged commit 4093af0 into main Aug 12, 2026
3 checks passed
@johngrantuk
johngrantuk deleted the rust-fix-1 branch August 12, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants