Introduction

Small rounding errors in automated markets can shift prices, drain pools, or create exploitable conditions. Balancer’s StableSwap vulnerability is one example of how a minor mathematical inconsistency can create a meaningful risk. This article explains how differential fuzzing with Python can reveal discrepancies between high-precision calculations and Solidity implementations.

Balancer Official report

Root Cause

The pool receives a swap amount and returns an amount of the other token. A precision problem occurs when the contract multiplies by a factor. The multiplication loses resolution and produces an output that is smaller than the mathematically correct value.

How to Fix Rounding

Two approaches can solve the issue:

  1. Round the input token in the correct direction
  2. Use the same value consistently for both calculation and transfer

Math in the Swap

The StableSwap Invariant

The StableSwap invariant is defined as:

A * n^n * S + D = A * D * n^n + D^(n+1) / (n^n * P)

Where:

  • A is the amplification coefficient, which shapes the curve toward constant sum behavior
  • n is the number of tokens in the pool
  • S is the sum of balances: Σ(x_i)
  • D is the invariant, similar to total liquidity
  • P is the product of balances: Π(x_i)

The contract uses this relationship to compute the output token amount when one balance changes.

For more details about the math, see this Cyfrin Updraft Curve course.

Note: In the Balancer code, amplificationParameter represents A multiplied by n^(n−1), not A alone. Tests must use this expanded value to avoid false mismatches.

This article focuses on testing the correctness of the math with differential fuzzing rather than explaining the mathematical structure itself. Some summaries of code may include AI-generated descriptions and should be reviewed carefully.

The appendix provides an example Python test for a Balancer pool.

The test includes:

  • Manually Guided Fuzzing (MGF) logic
  • Contract calls and their Python equivalents
  • Functions named *pure_math_quiet that use Python’s Decimal type for accurate calculations

How the differential test works:

  1. Python computes values with high precision using Decimal
  2. Solidity computes the same values
  3. The test compares the two results and checks whether the deviation is acceptable

The function get_token_balance_pure_math_quiet uses Python’s sqrt() for a more direct and accurate solution than the Newton iteration used in Solidity.

Important Points for Fuzzing

Standard fuzzing explores random values within a defined range. If that range does not include edge cases, the fuzzing campaign will not expose them. High-quality fuzzing requires values that reach extreme states, although the time spent testing those values may not always reveal issues efficiently.

Testing Edge Cases

Two fuzzing approaches are useful. First, test normal conditions with typical values. Second, test edge cases with extreme values to see where the system breaks.

Edge case testing answers essential questions. If an overflow occurs, can users still withdraw? Can the protocol pause and then resume correctly? The system must handle failure and recover without causing further loss.

A common mistake is to stop testing when an edge case fails. Instead, continue exploring to understand which components keep working and which ones do not.

Normal and edge-case testing can be combined, although this can create unnecessary complexity. Clear tests are usually better than complicated ones.

Testing Environments

Integration tests use real external protocols. These tests run slowly but reveal actual behavior. Fork tests create a local copy of mainnet state. Fork tests are much faster and allow modification of conditions that would never occur on mainnet.

Mainnet state rarely contains extreme values. When testing edge cases on a fork, you must create extreme conditions yourself.

Manually Guided Fuzzing (MGF) helps achieve this outcome. With MGF, the tester directs fuzzing toward specific scenarios that need exploration.

Off topic: Fuzzing on ERC-4626

The ERC-4626 vault standard contains precision-sensitive accounting for deposits, withdrawals, and share issuance. Large withdrawals can affect rounding and potentially cause fund loss. Fuzzing these vaults requires logic that monitors balances, supply, and share behavior. This makes ERC-4626 testing more demanding than standard fuzzing.

Conclusion

Testing swap functions requires two confirmations. The math must produce the correct output, and the contract must transfer that exact amount. Differential fuzzing helps verify both points.

Good fuzzing must reflect the mathematical formulas that define the system. Python is well-suited for this work because it offers high-precision tools. Differential tests compare the theoretical results with the contract’s behavior.

Several aspects must be verified. The invariant must remain correct. Output calculations must match when computed from different directions. All intermediate values must verify cleanly. This is the only reliable way to ensure the math and the implementation align.

Manually Guided Fuzzing (MGF) combines human insight with automated exploration. This approach finds subtle bugs that random fuzzing often misses.

Appendix

https://gist.github.com/meditationduck/5b51b49b23cda2220672bdd004f131b9