- November 7, 2025
OpenZeppelin
OpenZeppelin
The recent exploit of Balancer v2 Composable Stable Pools, which resulted in losses exceeding $120M, has understandably shaken the blockchain community. OpenZeppelin conducted security audits of earlier versions of the Balancer v2 codebase, prior to the introduction of the attack vector that was later exploited.
We're now sharing our technical analysis of the exploit to help educate the community about this complex attack and to advance the conversation about effective security best practices in our rapidly evolving industry.
This post is divided into two sections:
- Technical analysis of the attack
- Learnings and opportunities to improve blockchain security practices
1. Exploit Analysis
1.1 The Attack
On November 3, 2025 at around 7:40 AM UTC specific Balancer v2 stable pools were targeted with a sophisticated attack that resulted in a cumulative loss of $120+ million across Balancer and its forks.
The attack leveraged several circumstances:
- The majority of the targeted pools were instances of the
ComposableStablePoolcontract. A special pool designed for assets that are either expected to consistently swap at near parity, or at a known exchange rate. - The
batchSwapfunctionality of the BalancerVaultcontract, which allows for transient swaps to happen before the need to settle the outstanding deltas. - The attack was profitable in low liquidity scenarios. The attacker had to prepare large swaps so that pools’ balances could be brought to low liquidity of one or other tokens. The most used way of achieving this was to swap LP tokens for pool tokens, leaving the pool in a low liquidity state.
- All of the above are just necessary but not conclusive circumstances, with the only necessary circumstance for this to happen to be the root cause of the issue, a rounding behaviour in the
_upscalefunction.
The attack vector was introduced first on July 16, 2021 when MetaStablePool overrode the _scalingFactors function at commit 059284e. The same change has been later applied on September 1, 2021 for linear pools at commit 4e9e70a and on September 20, 2021 at commit f450760 for StablePhantomPool, later renamed ComposableStablePool. OpenZeppelin’s prior audits did not cover any of those pools presenting this attack vector.
In the issue description below, we explain why the introduction of this override was at the core of the attack.
1.2 The Issue
The batchSwap function of the Vault contract is the entry point for this attack. The attacker crafted a call to it, targeting instances of ComposableStablePool. The relevant logic in the Vault contract that brings to the ComposableStablePool interaction is defined by this flow:
batchSwaps → internally calls _swapWithPools → which internally calls a pool for each swap at _swapWithPool → _processGeneralPoolSwapRequest→ ultimately calls onSwap on the ComposableStablePool.
Once landed in ComposableStablePool, the execution continues within onSwap hook where two things happen:
- The call to
_scalingFactorsfunction. - The hand over to
_swapGivenOutas defined in the example transaction provided above. Notice that the attacker can have decided to take the_swapGivenInpattern but the provided inputs in the attack transaction clearly show the choice. This function will call the_upscalefunction.
Let's take a look at these two functions.
_scalingFactors
function _scalingFactors() internal view virtual override returns(uint256[] memory) {
uint256 totalTokens = _getTotalTokens();
uint256[] memory scalingFactors = new uint256[](totalTokens);
for (uint256 i = 0; i < totalTokens; ++i) {
scalingFactors[i] = _getScalingFactor(i).mulDown(_getTokenRate(i));
}
return scalingFactors;
}
This function does two things:
- It scales any amount to a fixed number of decimals as suggested by the
_getScalingFactorfunction and what it returns. - It factors in the token exchange rate by multiplying it by the scaling factor and dividing by 1e18.
_upscale
function _upscale(uint256 amount, uint256 scalingFactor) pure returns (uint256) {
/* Upscale rounding wouldn't necessarily always go in the same direction:
in a swap for example the balance of token in should be
rounded up, and that of token out rounded down. This
is the only place where we round in the same direction for all amounts,
as the impact of this rounding is expected to be minimal. */
return FixedPoint.mulDown(amount, scalingFactor);
}
Which just performs a multiplication of amount by its scaling factor, dividing by 1e18 at the end.
The issue lies in two facts:
- These functions always round-down (
mulDown) independently from the direction of the swap. - If amounts are orders of magnitude less than
scalingFactorsones, the precision loss becomes non-negligible. In the attacker transaction, a typical value of amount and scaling factor are:
amount: "17"
scalingFactor: "1058132408689971699"
If we do amount*scalingFactor / 1e18 we would obtain 17,98 but this would truncate to 17 in Solidity, leading to a 0% net change in the _upscale function. Conversely, if we now imagine an amount of 17000000000000000000 (a token with 18 decimals) we obtain 17988250950000000000 which represents a 5.8% increase (the rate indeed). Similarly, if we do a token with 6 decimals as most stablecoins 17000000 we would obtain 17988250 which is still roughly the same positive increment in percentage.
Truncation is maximized by making amount*scalingFactor mod 1e18 as large as possible, so the product loses the maximum precision when divided by 1e18.
For example, in the table below, amount=17 and amount=50 both produce an absolute rounding loss of about 0.90. However, error as a percentage of amount differs significantly, and the percent of increase lost diverges even more, since the same absolute loss is much larger relative to 17 than to 50.
Table 1. Examples of floor rounding error when scaling by factor 1.058132408689971699
The _swapGivenOut function will later use these rounded values to calculate the amount owed by the attacker to the pool after the swap. This value is artificially deflated, making the swap cheaper. The inner mechanics of the _swapGivenOut are complex and they also justify why this can be achieved in pools where low liquidity states can be obtained at will. This is due to an invariant check which is ensuring convergence to some degree. In high liquidity situations, big shifts in the balances would have failed due to the same invariant protection.
Ultimately, with a high number of iterations, the deltas after the batchSwap are inflated, crediting to the attacker the majority of the funds in the pool.
Additionally, there’s a pertinent observation to be made about the _upscale in-line docstrings:
/* Upscale rounding wouldn't necessarily always go in the same direction: in a swap for example the balance of token in should be rounded up, and that of token out rounded down. This is the only place where we round in the same direction for all amounts, as the impact of this rounding is expected to be minimal. */
A prior version of this comment had an additional note:
/* …as the impact of this rounding is expected to be minimal (and there's no rounding error unless `_scalingFactor()` is overriden). */
This is relevant since the StablePhantomPool introduced on September 20, 2021, which is later renamed to ComposableStablePool, implements exactly the override in _scalingFactor that we discussed above. Previously, in an old version of StablePool the _scalingFactor function only accounted for token decimals differences, and as already seen, now it factors in the exchange rate too. This is fundamentally different, since as the _upscale comment signals, the code went from unitary scaling factors (ie 1e12) to non-unitary exchange rates. This shift is what the _upscale warns against, opening the door for rounding errors and thus for the attack vector.
While enabling the rounding error was the root cause, exploiting it required leveraging additional protocol mechanics and specific attack steps.
Balancer’s batchSwap allows transient internal balances that are only net-settled at the end of the batch. Thanks to this, the attacker effectively “borrowed” BPT within a batch to manipulate the pool without needing to end the transaction holding BPT.
A BPT (Balancer Pool Token) is an ERC-20 that represents a pro-rata share of a Balancer pool. In some pool designs, including the targeted ComposableStablePools, the BPT can also appear as a pool asset and be tradable/swappable.
The first phase pushed pool token balances (e.g., WETH / osETH) to very low levels (≲ 100k) by repeatedly swapping BPT to token₁ and BPT to token₂.
With balances small, fixed-point rounding becomes dominant. The objective is to maximize the discarded fractional part in floor arithmetic, i.e., maximize amount*scalingFactor mod 1e18 so that the product loses as much precision as possible when divided by 1e18. At sufficiently low balances, the entire intended increment can be truncated away, this is where the maximum is attained and what the hacker looks for.
The exploit runs in repeating triplets of swaps:
- Prime: move the pool to a state where truncation will occur on the next operation. In the screenshot below, a swap of WETH to osETH
- Exploit: perform the swap that realizes the rounding loss. In the screenshot, a swap of 17 WETH for osETH.
- Reset: restore balances so the triplet can be replayed. In the screenshot is the swap of osETH to WETH.
As shown in the trace below, the critical swap often uses an amount=17 against a vault balance of 18. You can see the repeated 17 amounts in the second swap of each triplet.
Figure 1. BatchSwap trace: repeating triplets. Source.
1.3 Audit Scope in Relation to the Exploit
We believe it is important to provide specific details regarding OpenZeppelin’s audits of earlier versions of Balancer v2 due to misconceptions presented in the media regarding our involvement and security auditing in general.
The attack vector exists in code (ComposableStablePool and Linear pools) that was added after the OpenZeppelin audit concluded and was not part of our audit scope. The contracts the OpenZeppelin team audited (StablePool and WeightedPool) did not contain the _scalingFactors override that returns non-unitary values and enables this exploit.
OpenZeppelin conducted two audits of Balancer v2:
First Audit (February 8 - March 15, 2021)
- Commit: 1cb36eb56a6b7dbd70bfa3dc16b53357b43b9d5a
- Scope: StablePool, WeightedPool, and all their dependencies (BaseGeneralPool, BasePool), the Vault contract with its batch swap functionality, authorization, fee management, and pool factory contracts
- Audit report
Here the _upscale function presented the referenced comment, which assumed safety as long as no overrides were made to the _scalingFactors function.
Second Audit (August 23 - September 10, 2021)
- Commit: 7ff72a23bae6ce0eb5b134953cc7d5b79a19d099
- Scope: Changes to StablePool and StableMath implementations
- Audit report
The critical detail is what happened during and after our second audit. New contracts were added to the repository but were not within the scope of our engagement:
- LinearPool - added September 1, 2021
- PhantomStablePool (later renamed ComposableStablePool) - added September 20, 2021
These contracts introduced the crucial _scalingFactors override accounting for exchange rates, making the return values non-unitary anymore and thus enabling the attack vector. The StablePool and WeightedPool contracts we audited maintained the original _scalingFactors implementation that only handled decimal normalization - they did not include the exchange rate calculations that made the rounding behaviour exploitable.
The pools we audited never overrode _scalingFactors to include exchange rates. As the original code comment warned "the impact of this rounding is expected to be minimal (and there's no rounding error unless `_scalingFactor()` is overriden).", the rounding approach in _upscale was safe "unless _scalingFactor() is overridden", which it wasn't in the contracts we reviewed. The attack vector only manifests when:
- The
_scalingFactorfunction is overridden to include non-unitary exchange rates (as inComposableStablePool) - This override interacts with the existing
_upscalerounding behavior - The pool can be manipulated to low liquidity states where rounding errors become significant
2. Advancing Security Best Practices: Lessons for the Industry
This incident has sparked debate about the effectiveness of smart contract security audits, with some commentators questioning whether they provide value. While this sentiment may be understandable in the moment of frustration, it fails to acknowledge why security audits have become an industry best practice and the tremendous impact the security audit practice has had on mitigating risk and protecting users over the past decade.
Security firms collectively prevent hundreds of potential exploits each year. OpenZeppelin alone has identified over 700 critical and high-severity vulnerabilities before they reached production across all our audits. These prevented disasters don't make headlines, but they represent billions in protected value and countless users saved from losses. High-quality security audits also help development teams improve their security posture across the development lifecycle, resulting in an exponential reduction in the likelihood of further errors being introduced into blockchain applications.
The real lesson isn’t that audits are ineffective — it’s that the industry’s audit practices haven’t yet caught up with how fast complex protocols that secure significant value evolve. Because audits are often scoped as isolated reviews rather than continuous engagements, context can be lost as codebases evolve. Changing this requires a collective shift: protocol teams investing in ongoing security, and auditors building frameworks that support it.
2.1 Establishing Continuous Security as the Industry Standard
The Balancer v2 exploit illustrates why we've been advocating for a fundamental shift in how the industry approaches security. Since pioneering the practice of smart contract auditing in 2016, we've seen how the most successful security outcomes come from ongoing security partnerships that cover entire protocol codebases and all of their changes over time, rather than ad hoc code reviews of major upgrades, which are often limited in scope to only those changes.
When security researchers work with protocols continuously, they develop deep familiarity with the protocol's architecture, engineering processes, and why specific design decisions were made. This deep understanding significantly lowers risks when compared to ad hoc audits.
Although the Balancer v2 codebase was reviewed by four independent audit firms, each firm was engaged to focus on different scopes of the protocol. Engaging multiple auditors helps reduce the chance of missing vulnerabilities, but maintaining at least one long-term security partner provides a deeper, continuous understanding of how the codebase evolves and how new changes interact with existing logic.
2.2 Building Better Security Frameworks
Through our decade of experience, OpenZeppelin has consistently worked to establish and elevate security standards across the industry. OpenZeppelin Contracts have become the de facto standard for building secure smart contracts, currently powering over $30 trillion in total value transferred across the ecosystem. Just as these contracts set the foundation for secure development, we have been actively working on efforts to establish more robust standards for security assessment and continuous protocol protection.
In addition to long-term relationships with our clients, we're actively working to shape these standards at both the industry and regulatory levels. For example, OpenZeppelin contributes to industry standard setting bodies, like the Blockchain Security Standards Council, the Enterprise Ethereum Alliance and the International Standards Organization (ISO), to ensure that the security best practices our team and community have helped pioneer are available to the whole industry.
We also engage with regulators and policymakers globally to ensure blockchain security best practices are promoted in relevant regulatory regimes. In particular, OpenZeppelin has engaged with the U.S. Department of Treasury, the U.S. Securities and Exchange Commission, the UK Financial Conduct Authority, and the French ACPR and AMF regarding the benefits of conducting comprehensive security audits on a regular basis. In addition, we have explored the potential to create a dedicated self-regulatory organization similar to auditors in other fields that we believe could help formalize security auditing standards and methodologies for blockchain technology, set quality and ethics requirements for auditors, and manage accreditation to certify qualified auditors, which could improve security outcomes and increase consumer confidence.
2.3 Strengthening the Blockchain Security Ecosystem Together
Every security incident provides valuable insights that drive improvement across our industry. The Balancer v2 exploit reinforces that as protocols become more valuable and sophisticated, it becomes critical for protocol teams to invest in ongoing security, as well as auditing firms to build frameworks that support it. Together, we can create security practices that are as innovative and robust as the technology they protect.