Research journal · Incident analysis
Balancer Incident Analysis and Differential Fuzzing
Learn how Balancer’s StableSwap math failed and how differential fuzzing in Python detects precision bugs in DeFi swap logic for safer smart contract design.
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
Vocabulary
Terms used in this article.
-
Differential fuzzing
Differential fuzzing sends generated inputs or action sequences through two implementations and flags behavior that diverges unexpectedly.
-
Fuzz testing
Automated testing that explores many generated inputs or action sequences to find violations of expected properties.
-
Invariant
A property that must remain true before and after every relevant state transition in a system.
-
Manually guided fuzzing
Manually guided fuzzing is stateful fuzz testing in which a researcher writes domain-specific actions, preconditions, state models, and invariants.
-
Protocol
A blockchain protocol is a defined set of rules and software interfaces that participants follow to reach consensus or provide an onchain application service.
-
Solidity
Solidity is a statically typed, object-oriented language for writing smart contracts that compile to Ethereum Virtual Machine bytecode.
Topics