{"id":1216,"date":"2025-11-27T14:33:25","date_gmt":"2025-11-27T12:33:25","guid":{"rendered":"https:\/\/ackee.xyz\/blog\/?p=1216"},"modified":"2025-12-08T15:45:16","modified_gmt":"2025-12-08T13:45:16","slug":"balancer-incident-analysis-and-differential-fuzzing","status":"publish","type":"post","link":"https:\/\/ackee.xyz\/blog\/balancer-incident-analysis-and-differential-fuzzing\/","title":{"rendered":"Balancer Incident Analysis and Differential Fuzzing"},"content":{"rendered":"<h2 class=\"code-line\" dir=\"auto\">Introduction<\/h2>\n<p data-start=\"132\" data-end=\"306\">Small rounding errors in automated markets can shift prices, drain pools, or create exploitable conditions. Balancer\u2019s 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.<\/p>\n<p class=\"code-line\" dir=\"auto\"><a class=\"decorated-link\" href=\"https:\/\/x.com\/Balancer\/status\/1986104426667401241\" target=\"_blank\" rel=\"noopener\">Balancer Official report<\/a><\/p>\n<h2 class=\"code-line\" dir=\"auto\">Root Cause<\/h2>\n<p data-start=\"308\" data-end=\"563\">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.<\/p>\n<h3 class=\"code-line\" dir=\"auto\">How to Fix Rounding<\/h3>\n<p dir=\"auto\">Two approaches can solve the issue:<\/p>\n<ol class=\"code-line\" dir=\"auto\">\n<li>Round the input token in the correct direction<\/li>\n<li>Use the same value consistently for both calculation and transfer<\/li>\n<\/ol>\n<h2 class=\"code-line\" dir=\"auto\">Math in the Swap<\/h2>\n<h3 class=\"code-line\" dir=\"auto\">The StableSwap Invariant<\/h3>\n<p data-start=\"565\" data-end=\"751\">The StableSwap invariant is defined as:<\/p>\n<p class=\"code-line\" dir=\"auto\"><code class=\"codehl\">A * n^n * S + D = A * D * n^n + D^(n+1) \/ (n^n * P)<\/code><\/p>\n<p dir=\"auto\">Where:<\/p>\n<ul class=\"code-line\" dir=\"auto\">\n<li><code class=\"codehl\">A<\/code> is the amplification coefficient, which shapes the curve toward constant sum behavior<\/li>\n<li><code class=\"codehl\">n<\/code> is the number of tokens in the pool<\/li>\n<li><code class=\"codehl\">S<\/code> is the sum of balances: \u03a3(x_i)<\/li>\n<li><code class=\"codehl\">D<\/code> is the invariant, similar to total liquidity<\/li>\n<li><code class=\"codehl\">P<\/code> is the product of balances: \u03a0(x_i)<\/li>\n<\/ul>\n<p data-start=\"933\" data-end=\"1074\">The contract uses this relationship to compute the output token amount when one balance changes.<\/p>\n<p data-start=\"1076\" data-end=\"1144\">For more details about the math, see this Cyfrin Updraft <a href=\"https:\/\/updraft.cyfrin.io\/courses\/curve-v1\" target=\"_blank\" rel=\"noopener\">Curve course<\/a>.<\/p>\n<p data-start=\"1146\" data-end=\"1220\"><strong>Note<\/strong>: In the Balancer code, <code class=\"codehl\" data-start=\"1695\" data-end=\"1719\">amplificationParameter<\/code> represents A multiplied by n^(n\u22121), not A alone. Tests must use this expanded value to avoid false mismatches.<\/p>\n<p data-start=\"1832\" data-end=\"2070\">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.<\/p>\n<p data-start=\"2072\" data-end=\"2137\">The appendix provides an example Python test for a Balancer pool.<\/p>\n<p dir=\"auto\">The test includes:<\/p>\n<ul class=\"code-line\" dir=\"auto\">\n<li><a href=\"https:\/\/ackee.xyz\/blog\/introducing-manually-guided-fuzzing-a-new-approach-in-smart-contract-testing\/\" target=\"_blank\" rel=\"noopener\">Manually Guided Fuzzing (MGF)<\/a> logic<\/li>\n<li>Contract calls and their Python equivalents<\/li>\n<li>Functions named <code class=\"codehl\" data-start=\"2265\" data-end=\"2282\">*pure_math_quiet<\/code> that use Python\u2019s <code class=\"codehl\" data-start=\"2301\" data-end=\"2310\">Decimal<\/code> type for accurate calculations<\/li>\n<\/ul>\n<p dir=\"auto\">How the differential test works:<\/p>\n<ol class=\"code-line\" dir=\"auto\">\n<li>Python computes values with high precision using <code class=\"codehl\" data-start=\"2432\" data-end=\"2441\">Decimal<\/code><\/li>\n<li>Solidity computes the same values<\/li>\n<li>The test compares the two results and checks whether the deviation is acceptable<\/li>\n<\/ol>\n<p data-start=\"2571\" data-end=\"2726\">The function <code class=\"codehl\" data-start=\"2584\" data-end=\"2619\">get_token_balance_pure_math_quiet<\/code> uses Python\u2019s <code data-start=\"2634\" data-end=\"2642\">sqrt()<\/code> for a more direct and accurate solution than the Newton iteration used in Solidity.<\/p>\n<h3 class=\"code-line\" dir=\"auto\">Important Points for Fuzzing<\/h3>\n<p data-start=\"2052\" data-end=\"2180\">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.<\/p>\n<h2 class=\"code-line\" dir=\"auto\">Testing Edge Cases<\/h2>\n<p data-start=\"3086\" data-end=\"3247\">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.<\/p>\n<p data-start=\"3249\" data-end=\"3467\">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.<\/p>\n<p data-start=\"3469\" data-end=\"3624\">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.<\/p>\n<p data-start=\"3626\" data-end=\"3774\">Normal and edge-case testing can be combined, although this can create unnecessary complexity. Clear tests are usually better than complicated ones.<\/p>\n<h3 class=\"code-line\" dir=\"auto\">Testing Environments<\/h3>\n<p data-start=\"3801\" data-end=\"4046\">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.<\/p>\n<p data-start=\"4048\" data-end=\"4173\">Mainnet state rarely contains extreme values. When testing edge cases on a fork, you must create extreme conditions yourself.<\/p>\n<p data-start=\"4175\" data-end=\"4318\">Manually Guided Fuzzing (MGF) helps achieve this outcome. With MGF, the tester directs fuzzing toward specific scenarios that need exploration.<\/p>\n<h3 class=\"code-line\" dir=\"auto\">Off topic: Fuzzing on ERC-4626<\/h3>\n<p data-start=\"3432\" data-end=\"3600\">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.<\/p>\n<h2 class=\"code-line\" dir=\"auto\">Conclusion<\/h2>\n<p data-start=\"4711\" data-end=\"4899\">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.<\/p>\n<p data-start=\"4901\" data-end=\"5133\">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\u2019s behavior.<\/p>\n<p data-start=\"5135\" data-end=\"5399\">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.<\/p>\n<p data-start=\"5401\" data-end=\"5547\">Manually Guided Fuzzing (MGF) combines human insight with automated exploration. This approach finds subtle bugs that random fuzzing often misses.<\/p>\n<h2 class=\"code-line\" dir=\"auto\">Appendix<\/h2>\n<p class=\"code-line\" dir=\"auto\"><a class=\"decorated-link\" href=\"https:\/\/gist.github.com\/meditationduck\/5b51b49b23cda2220672bdd004f131b9\" target=\"_blank\" rel=\"noopener\">https:\/\/gist.github.com\/meditationduck\/5b51b49b23cda2220672bdd004f131b9<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Small rounding errors in automated markets can shift prices, drain pools, or create exploitable conditions. Balancer\u2019s 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&hellip;<\/p>\n","protected":false},"author":24,"featured_media":1222,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[61,10,85,84],"tags":[14,28,104],"class_list":["post-1216","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-education","category-ethereum","category-exploits","category-hacks","tag-exploit","tag-smart-contract","tag-wake"],"aioseo_notices":[],"featured_image_src":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2025\/11\/Balancer-600x400.jpg","featured_image_src_square":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2025\/11\/Balancer-600x600.jpg","author_info":{"display_name":"Naoki Yoshida","author_link":"https:\/\/ackee.xyz\/blog\/author\/naoki-yoshida\/"},"_links":{"self":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/1216","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/users\/24"}],"replies":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/comments?post=1216"}],"version-history":[{"count":0,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/1216\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media\/1222"}],"wp:attachment":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media?parent=1216"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/categories?post=1216"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/tags?post=1216"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}