Full Report
Balancer had a read only reentrancy vulnerability. This happens when a read only function, outside of the protocol, calling back into the function within a weird state. The reentrancy checks don't work, since it's normally not put onto view functions. The vulnerability comes from the function _joinOrExit. It invokes the function _callPoolBalanceChange before calling _setMinimalSwapInfoPoolBalances. This is important because _setMinimalSwapInfoPoolBalances updates the token balances for balancer. The interesting part is that the protocol will always send back unused Ether to the user part way through execution via sendValue. The transferring of the ETH is in the middle of the execution of _callPoolBalanceChange but before _setMinimalSwapInfoPoolBalances. This means that the Balancer pool tokens have been minted but the vault has NOT been updated at this stage. This puts the contract into a funky state. So, reentrancy to steal all of the money with consecutive calls, right? Well, not so fast. These functions have reentrancy modifiers on them in order to prevent this. The modifiers are only placed on external functions that modify the state typically. This still means a read only reentrancy is possible though. If an attacker sets up a fallback function where the extra ETH gets sent back to, they could make read only calls to the contract using this exploit. Further analysis of this vulnerability can be found on the Balancer Forums. What even uses these read only calls though? Pricing oracles! In the case of Sentiment.xyz, they were using Balancer as an oracle for how much each token costs. The attacker performed the following steps to exploit this: Take out a flash loan and put assets into Sentiment.xyz as collateral. Manipulate the Balancer pool. Setup a reentrant contract to call the exitPool function. Call Sentiment.xyz with the manipulated pool prices to obtain collateral back at an extremely high cost. Profit. I'm a little confused on how Sentiment.xyz got exploited on this, since this is an issue with Balancer and Balancer patched the problems. My hypothesis is that Sentiment.xyz was using an older version of the pool that was vulnerable or was not using the information from the contract properly. Overall, super interesting vulnerability!
Analysis Summary
# Vulnerability: Read-Only Reentrancy in Balancer Vault
## CVE Details
- **CVE ID**: N/A (Commonly referred to in DeFi as the Balancer Read-Only Reentrancy flaw)
- **CVSS Score**: 9.6 (Critical - *Estimated based on protocol impact*)
- **CWE**: CWE-706: Use of Incorrectly-Resolved Name or Reference; CWE-841: Improper Enforcement of Behavioral Workflow.
## Affected Systems
- **Products**: Balancer V2 Vault and associated Liquidity Pools.
- **Versions**: Legacy Balancer V2 pools utilizing `_joinOrExit` logic.
- **Configurations**: Protocols (like Sentiment.xyz) using Balancer LP token pricing as a price oracle for collateral valuation.
## Vulnerability Description
The flaw is a **Read-Only Reentrancy** bug located within the Balancer Vault's `_joinOrExit` execution flow.
1. The function invokes `_callPoolBalanceChange` before updating internal state via `_setMinimalSwapInfoPoolBalances`.
2. During `_callPoolBalanceChange`, the protocol returns unused ETH to the user via `sendValue`.
3. This ETH transfer triggers a `fallback()` function in the caller's contract **before** the Vault has updated its total token balances.
4. While the Vault is protected by reentrancy guards for state-changing calls, **view/read-only functions** (like those calculating pool prices) do not have these guards.
5. Consequently, an external contract can query the pool price during the fallback execution, receiving a manipulated value because the pool tokens have been minted/burned but the asset balances are not yet synchronized.
## Exploitation
- **Status**: **Exploited in the Wild** (Sentiment.xyz exploit, approximately $1M loss).
- **Complexity**: High
- **Attack Vector**: Network (Smart Contract Interaction)
## Impact
- **Confidentiality**: None
- **Integrity**: Critical (Price data manipulation leads to unauthorized borrowing/withdrawal).
- **Availability**: Low
## Remediation
### Patches
- Balancer has updated core Vault logic and deployed patched versions of pool factories.
- Integration of the `whenNotPaused` and enhanced reentrancy guards for sensitive state-checks.
### Workarounds
- **Oracle Side**: Integrate "Reentrancy-aware" oracle queries. Oracles should check the Balancer Vault's reentrancy lock status (e.g., calling a state-changing function that is guarded) before trusting the price.
- **Syncing**: Ensure internal systems do not rely on the mid-block state of external AMMs.
## Detection
- **Indicators of Compromise**: Multiple Flash Loan transactions followed by a series of `joinPool`/`exitPool` calls within a single transaction, paired with unexpected collateral shifts on lending platforms.
- **Detection Methods**: Monitor for calls to `fallback()` or `receive()` functions that subsequently query pool prices within the same transaction hash.
## References
- Balancer Forum: hxxps[://]forum[.]balancer[.]fi/
- Sentiment.xyz Post-Mortem: hxxps[://]twitter[.]com/sentimentxyz
- ChainSecurity Reentrancy Guide: hxxps[://]chainsecurity[.]com/heartbeats/read-only-reentrancy/