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.
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:
- Round the input token in the correct direction
- 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:
Ais the amplification coefficient, which shapes the curve toward constant sum behaviornis the number of tokens in the poolSis the sum of balances: Σ(x_i)Dis the invariant, similar to total liquidityPis 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_quietthat use Python’sDecimaltype for accurate calculations
How the differential test works:
- Python computes values with high precision using
Decimal - Solidity computes the same values
- 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