On August 22, Balancer officially announced the detection of a severe vulnerability affecting multiple V2 Boost pools, with only 1.4% of the Total Value Locked (TVL) impacted. Several pools were paused temporarily, and users were urged to withdraw their Liquidity Provider (LP) assets promptly【1】【2】.
On August 27, the MistEye system by SlowMist detected suspicious transactions exploiting the Balancer vulnerability【3】.
As pausing the pools wasn’t entirely effective, some funds remained vulnerable to the attacks. Balancer once again alerted users to retrieve their LP assets from the affected pools【4】. Subsequently, Balancer released the details of the August-disclosed vulnerability on Medium【5】, which our security team has reviewed and analyzed, as elaborated below.
In its disclosure, Balancer pinpointed that the issue stemmed from the rounding down in linear pools and the virtual supply in composable pools leading to a zero bptSupply. Let’s first delve into a brief understanding of the Balancer protocol content related to this vulnerability.
Balancer V2 Vault
The Balancer V2 protocol【6】is a decentralized Automated Market Maker (AMM) based on Ethereum, representing a flexible building block for programmable liquidity. At its core lies the Vault contract, which maintains records of all pools, manages token accounting and transfers, including wrapping and unwrapping of native ETH. Essentially, the Vault implementation separates token accounting and management from pool logic.
Within the Vault, there are four interfaces: joinPool, exitPool, swap, and batchSwap (joining, exiting, and swapping are distinct calls, with no combinations in a single call). A notable feature is batchSwap, enabling multiple atomic swaps between pools, connecting the output of one pool to the input of another (GiveIn and GiveOut). The system also incorporates flash swaps【7】, akin to an internal flash loan mechanism.
In a bid to enhance the capital efficiency for Liquidity Providers (LP) and address the high costs associated with wrapping and unwrapping, Balancer introduced Linear Pools in V2, consequently launching the BPT (ERC20 Balancer Pool Token).
Linear Pools【8】comprise the main token (underlying asset), wrapped token, and BPT token, facilitating asset exchanges and their wrapped, yield-bearing counterparts at a known exchange rate. A higher proportion of wrapped tokens augments the yield and capital efficiency of the fund pool. During the wrapping process, scaling factors are typically employed to ensure consistent precision across different tokens.
All Balancer pools are composable, containing other tokens, and have their own tokens as well. Here, BPT refers to the ERC20 Balancer Pool Token, forming the foundation for all pools. Users can freely compose exchanges using BPT tokens within other pools. An exchange always involves a pool and two tokens: GiveIn and GiveOut. ‘GiveIn’ denotes contributing a constituent token and receiving BPT, while ‘GiveOut’ signifies contributing BPT and receiving a constituent token. If BPT is a constituent token itself, it can be exchanged like any other token. This setup creates a simple batchSwap pathway between the underlying assets and tokens in external pools. Users can exchange BPT for the underlying assets of Linear Pools, which is also the fundamental premise of Balancer Boosted Pool【9】.
With the aforementioned configurations, the composable pools of Balancer have been established. A bb-a-USD composable stable pool is formed by three linear pools, simultaneously channeling idle liquidity to external protocols like Aave. For instance, bb-a-DAI is a linear pool comprising DAI and waDAI (wrapped aDAI). When users need to perform a batchSwap (e.g., exchanging USDT for DAI), the exchange pathway is as follows:
1. In the USDT linear pool, exchange USDT for bb-a-USDT (entering the USDT linear pool);
2. In bb-a-USD, exchange bb-a-USDT for bb-a-DAI (exchange between linear BPTs);
3. In the DAI linear pool, exchange bb-a-DAI for DAI (exiting the DAI linear pool).
Having briefly acquainted ourselves with the above prerequisites, we now proceed to the vulnerability analysis segment.
On August 27, the SlowMist security team received a notification from the MistEye system about a transaction that appeared to exploit a vulnerability in Balancer【3】.
The attacker initially borrowed 300,000 USDC via a flash loan from AAVE, then invoked the Vault’s batchSwap function, utilizing the composable stable pool bb-a-USD for BPT token exchange calculations. Ultimately, they exchanged 94,508 USDC for 59,964 bb-a-USDC, 68,201 bb-a-DAI, and 74,280 bb-a-USDT. Finally, the obtained BPT tokens were exited from the pool via the Vault contract’s exitPool function to acquire the underlying assets, repay the flash loan, and exit with a profit of approximately 108,843.7 USD.
It’s evident that the crux of this attack lies in the batchSwap function, but what exactly transpired within batchSwap? Let’s dive a little deeper.
During the batchSwap process, the attacker first exchanged USDC in the bb-a-USDC pool, then proceeded with BPT token exchanges, converting bb-a-USDC to bb-a-DAI, bb-a-USDT, and USDC. Finally, the underlying main token USDC was exchanged for bb-a-USDT. In other words, bb-a-USDC, as a crucial BPT token, served as the constituent token for both GiveOut and GiveIn.
In the first step, the attacker used a fixed scaling factor to exchange BPT tokens for USDC main tokens in the bb-a-USDC linear pool, with the increased amount recorded in the pool’s bptBalance. However, after the second onSwap exchange, we noticed that the same exchange process yielded an amountOut value of 0 for USDC. Why is that?
Upon scrutinizing the onSwap function, we found that an initial precision handling, or normalization, was conducted to calculate the respective token’s scaling factor. Subsequently, while invoking the _downscaleDown function, there was a rounding down scenario with amountOut. If there is a significant difference between amountOut and scalingFactors[indexOut], the computed _downscaleDown value would be zero.
When we use BPT tokens to exchange for main tokens, if the amountOut is too small, the returned value will be rounded down to zero, especially if this value is less than 1e12 as calculated by scalingFactors. However, the amountIn of bb-a-USDC coming in will still be added to the bptBalance virtual quantity, and this operation will increase the balance in the bb-a-USDC pool. It can be viewed as adding liquidity to the bb-a-USDC pool from one side.
Continuing on, leveraging the characteristics of the composable stable pool, the attacker initiated the interchange between BPT tokens, starting by exchanging bb-a-USDC for other BPT tokens. Following this exchange process, the composable stable pool’s call path — bb-a-DAI onSwap -> _swapGivenIn -> _onSwapGivenIn — sequentially converted bb-a-USDC into bb-a-DAI and bb-a-USDT. Unlike in linear pools, composable stable pools require an update of the exchange rate cache before executing the onSwap operation. From the code, we can observe that within the composite pool, onSwap will first ascertain if there’s a need to update the cached token exchange rate.
Following the previous exchanges, the quantity of bb-a-USDC altered and post nominalization via _toNominal, the actual total balance amounted to totalBalance 994,010,000,000, with a virtual supply of BPT tokens at 20,000,000,000. It can be deduced that the updated exchange rate is nearly 45 times the original linear pool cache exchange rate of 1,100,443,876,587,504,549, equating to 49,700,500,000,000,000,000.
Subsequently, bb-a-USDC was exchanged for USDC in the linear pool. However, similar to the second exchange, this transaction once again resulted in a situation where amountOut was rounded down to 0, with the exchange pathway remaining the same as before.
The following exchange was a reverse operation where USDC was exchanged for bb-a-USDC, with the exchange path being onSwap -> onSwapGivenIn -> _swapGivenMainIn. During this process, we discovered that while calculating the amountOut required for exchange, the computation for the virtual supply was based on the difference between the BPT token totalsupply post-exchange and the remaining amount in the pool, which in this case, amounted to 0.
Due to bptSupply being 0, when calculating BPT Out, it directly invokes the _toNominal function. This pathway of invocation results in a nearly 1:1 exchange ratio between USDC and bb-a-USDC. This showcases how the attacker could leverage the functionalities in the protocol to manipulate the exchange rates, hence exploiting the system for profit. This analysis is crucial for understanding the underlying vulnerability and for deriving measures to prevent similar occurrences in the future.
The batchSwap function facilitates multiple atomic swaps between different pools, connecting the output of one pool to the input of another (tokenIn and tokenOut), and exchanging USDC for BPT tokens. Within this batchSwap, actual token transfers do not occur; instead, the final exchange quantities are confirmed by recording the amounts of incoming and outgoing tokens. Since linear pools execute exchanges through underlying asset tokens, with a fixed algorithm computing the Rate based on a virtual supply, two security vulnerabilities were identified in batchSwap:
1. The rounding down issue in linear pools: Attackers can unilaterally add main tokens to the pool through rounding down, thereby manipulating the token exchange rate within the corresponding composable pool.
2. The virtual supply characteristic of composable pools: The virtual supply is computed by subtracting the pool’s balance from BPT tokens. During an exchange, if GiveIn is BPT tokens, the subsequent supply will be deducted by this amount. Attackers only need to use BPT as GiveIn for the exchange, manipulate its supply to 0 first, and then perform a reverse swap, with BPT acting as GiveOut. At this juncture, as the supply is 0, the algorithm will execute the actual exchange at a nearly 1:1 ratio, lower than the linear pool’s exchange rate, indirectly manipulating the quantity of BPT tokens given out in GiveOut.
We observed that vulnerability one increased the exchange rate for the conversion, while vulnerability two, during a reverse exchange, reduced the exchange rate. By exploiting both vulnerabilities — essentially a double buff — the attacker profited and exited the scenario. This analysis sheds light on the systemic loopholes that were capitalized on, providing a foundation for bolstering the security measures to preempt similar exploits in the future.
SlowMist is a blockchain security firm established in January 2018. The firm was started by a team with over ten years of network security experience to become a global force. Their goal is to make the blockchain ecosystem as secure as possible for everyone. They are now a renowned international blockchain security firm that has worked on various well-known projects such as Huobi, OKX, Binance, imToken, Crypto.com, Amber Group, Klaytn, EOS, 1inch, PancakeSwap, TUSD, Alpaca Finance, MultiChain, Cheers UP, etc.
SlowMist offers a variety of services that include by are not limited to security audits, threat information, defense deployment, security consultants, and other security-related services. They offer AML (Anti-money laundering) software, Vulpush (Vulnerability monitoring) , SlowMist Hacked (Crypto hack archives), FireWall.x (Smart contract firewall) , Safe Staking and other SaaS products. They have partnerships with domestic and international firms such as Akamai, BitDefender, FireEye, RC², TianJi Partners, IPIP, etc.
By delivering a comprehensive security solution customized to individual projects, they can identify risks and prevent them from occurring. Their team was able to find and publish several high-risk blockchain security flaws. By doing so, they could spread awareness and raise the security standards in the blockchain ecosystem.