# Balancer V2 ComposableStablePool Rounding/Invariant Manipulation Exploit (Nov 2025)

> On 2025-11-03 (~07:40 UTC), Balancer v2 ComposableStablePool instances and Balancer v2 forks were drained via a unidirectional mulDown rounding flaw in _upscale, exposed by _scalingFactors overrides carrying non-unitary exchange rates and chained through batchSwap. Cumulative losses were reported at $120M+ (about $128M in some sources); no CVE was assigned and no attacker identity has been disclosed.

- **Published:** 2026-11-02T17:51:06Z
- **Last reviewed:** 2026-11-02T17:51:06Z
- **Canonical:** https://intel.threadlinqs.com/threat/TL-2026-5075
- **Card image:** https://intel.threadlinqs.com/og/threat/TL-2026-5075.png
- **ID:** TL-2026-5075
- **Severity:** CRITICAL
- **Category:** VULNERABILITY
- **Status:** ACTIVE
- **Detections:** 9 (1 shown free below) · **IOCs:** 19

## Description

Balancer v2 stable-style pools (ComposableStablePool, formerly StablePhantomPool, plus LinearPool and MetaStablePool) share a scaling-helper design in which token amounts are upscaled by _scalingFactors using mulDown. A code comment in the helper states that the impact of this rounding is expected to be minimal, and that there is no rounding error unless _scalingFactors() is overridden. The overrides, which add rate-provider exchange rates on top of decimal normalization, were introduced in MetaStablePool (2021-07-16, commit 059284e), LinearPool (2021-09-01, commit 4e9e70a) and StablePhantomPool/ComposableStablePool (2021-09-20, commit f450760), after the OpenZeppelin audit engagements had ended (OpenZeppelin analysis). With non-unitary scaling factors, truncation becomes material when amounts are tiny relative to the factor. OpenZeppelin's example: an amount of 17 with a scaling factor of 1,058,132,408,689,971,699 upscales to 17 rather than ~17.98.

Exploit mechanics (OpenZeppelin, Check Point, Rekt): the attacker called the Vault's batchSwap, using the transient internal-balances mechanism to borrow BPT inside the batch and defer net settlement. The first phase swapped BPT for pool tokens (e.g. WETH/osETH) to push balances down to very small values. Repeated prime/exploit/reset sequences then executed EXACT_OUT swaps with tiny amounts (e.g. 17 against a vault balance of ~18), so the rounding loss landed on the pool. Check Point counted 65 micro-swaps in a single atomic batchSwap. The compounded precision loss desynchronized the invariant and suppressed the BPT price, and inflated deltas credited most of the pool's assets to the attacker. The invariant protection only held at high liquidity, which is why draining pools to minimal balances was a prerequisite. Balancer's preliminary post-mortem, as reported by Cointelegraph, described rounding in EXACT_OUT swaps on Stable Pools and Composable Stable v5 pools combined with BatchSwap and flash loans, with exploited funds often remaining in the Vault as internal balances before withdrawal.

Impact: Check Point reports $128.64M lost across six chains in under 30 minutes. Rekt reports ~$93.4M in primary losses plus ~$18M from forks and copycats, and ~$38.4M recovered. Chains named across sources include Ethereum, Base, Avalanche, Gnosis, Berachain, Polygon, Sonic, Arbitrum and Optimism. Reported recoveries include StakeWise's multisig recovery of 5,041 osETH and ~13,495 osGNO, and a white-hat rescue of $12.86M on Berachain's BEX. Cointelegraph, citing Cyvers, says the attacker funded operations through Tornado Cash deposits. Per Rekt, copycat exploits against Balancer v2 forks began at 08:39 UTC. Source figures conflict on totals and attribution of recovery amounts. Both are as reported by each source.

Defender relevance: the flaw is logic/arithmetic, not memory corruption, so detection is on-chain and behavioral: Vault batchSwap calls with GIVEN_OUT swaps involving a pool's own BPT, pool token balances driven to very low values, and clusters of near-identical micro-swaps. Monitor any Balancer v2 fork that inherits the _scalingFactors override pattern.

## MITRE ATT&CK

- T1588.006 Vulnerabilities
- T1587.004 Exploits
- T1190 Exploit Public-Facing Application
- T1565.001 Stored Data Manipulation
- T1657 Financial Theft

## Free detection (Splunk SPL, Microsoft KQL, Sigma)

**Balancer v2 BatchSwap GIVEN_OUT Dust Micro-Swap Burst Against Pool BPT (Exploitation)** (`TL-2026-5075-SPL-001`)

Behaviour-first search for the Balancer v2 rounding exploit (TL-2026-5075): one batchSwap transaction with 30+ GIVEN_OUT Vault swaps, a leg trading the pool's own BPT and 10+ dust-sized outputs. The exploit used 65 micro-swaps with amounts such as 17 inside one atomic batchSwap (primary Ethereum tx 0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742). Covers ComposableStablePool, MetaStablePool, LinearPool and forks inheriting the _scalingFactors mulDown pattern.

- **Languages:** Splunk SPL, Microsoft KQL, Sigma
- **ATT&CK:** T1190, T1565.001, T1565, T1588.006
- **Data sources:** splunk index=blockchain sourcetype=evm:balancer:vault_swap, sentinel table BalancerVaultSwaps_CL, sigma product=evm service=balancer_vault
- **Severity:** critical
- **Confidence:** high

### Splunk SPL

```spl
index=blockchain sourcetype="evm:balancer:vault_swap" call_name="batchSwap" swap_kind="GIVEN_OUT" earliest=-24h
| eval pool_addr=lower(substr(pool_id,1,42)), amt_out=tonumber(amount_out)
| eval bpt_leg=if(lower(token_in)==pool_addr OR lower(token_out)==pool_addr,1,0), dust=if(amt_out<=100,1,0)
| stats count AS swap_count sum(bpt_leg) AS bpt_swaps sum(dust) AS dust_swaps dc(pool_id) AS pools dc(amount_out) AS distinct_amounts min(_time) AS first_seen BY chain, tx_hash, tx_from
| where swap_count>=30 AND bpt_swaps>=1 AND dust_swaps>=10
| eval signal="TL-2026-5075 Balancer v2 batchSwap GIVEN_OUT rounding exploit (T1190, T1565.001)", first_seen=strftime(first_seen,"%Y-%m-%d %H:%M:%S")
| table first_seen chain tx_hash tx_from swap_count bpt_swaps dust_swaps distinct_amounts pools signal
```

### Microsoft KQL

```kql
BalancerVaultSwaps_CL
| where TimeGenerated > ago(24h)
| where CallName == "batchSwap" and SwapKind == "GIVEN_OUT"
| extend PoolAddr = tolower(substring(PoolId, 0, 42)), AmtOut = todecimal(AmountOut)
| extend BptLeg = iff(tolower(TokenIn) == PoolAddr or tolower(TokenOut) == PoolAddr, 1, 0), Dust = iff(AmtOut <= 100, 1, 0)
| summarize SwapCount = count(), BptSwaps = sum(BptLeg), DustSwaps = sum(Dust), Pools = dcount(PoolId), DistinctAmounts = dcount(AmountOut), FirstSeen = min(TimeGenerated) by Chain, TxHash, TxFrom
| where SwapCount >= 30 and BptSwaps >= 1 and DustSwaps >= 10
| extend Signal = "TL-2026-5075 Balancer v2 batchSwap GIVEN_OUT rounding exploit (T1190, T1565.001)"
| project FirstSeen, Chain, TxHash, TxFrom, SwapCount, BptSwaps, DustSwaps, DistinctAmounts, Pools, Signal
```

### Sigma

```yaml
title: Balancer v2 BatchSwap GIVEN_OUT Dust Micro-Swap Burst Against Pool BPT
id: 7c1f3a52-4d8e-4b19-9a62-5e0c8d3b71a4
status: experimental
description: Detects a single Balancer v2 Vault batchSwap transaction containing 30+ GIVEN_OUT swaps, at least one leg trading the pool's own BPT and 10+ dust-sized amounts. Matches the ComposableStablePool mulDown rounding exploit of 2025-11-03 and copycats against forks. Fields are derived by the blockchain ETL enrichment.
references:
  - https://www.openzeppelin.com/news/understanding-the-balancer-v2-exploit
  - https://research.checkpoint.com/2025/how-an-attacker-drained-128m-from-balancer-through-rounding-error-exploitation/
  - https://rekt.news/balancer-rekt2
  - https://medium.com/balancer-protocol/nov-3-exploit-post-mortem-51dcbeb6b020
author: AII-Detector — ThreadLinqs Intelligence
date: 2026/11/02
tags:
  - attack.initial-access
  - attack.t1190
  - attack.impact
  - attack.t1565
  - attack.t1565.001
  - attack.resource-development
  - attack.t1588.006
  - detection.threat-hunting
logsource:
  product: evm
  service: balancer_vault
detection:
  selection_behavior:
    call_name: batchSwap
    swap_kind: GIVEN_OUT
    swap_count|gte: 30
    bpt_leg_count|gte: 1
    dust_swap_count|gte: 10
  selection_known_tx:
    tx_hash: ['0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742', '0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569', '0xe4dfc8b8b54eb7e101d59cd9f87f389186b2e8f6e188557ae9dfdbea2b12e703', '0x29135f912d67db38478d0be70b9f2a1fab3b121b74d776f835ac66d6df134ec5', '0x3c9d2d16404a79feed9876a79f168af334726ad3ee1371f581d50ebebfe6b8c6', '0x167993d4cc39771923a6cd11d2d6e73a1b68c7464ea3c76ba41fbd32df7a96da', '0xd7996c8e187b9bd539a04a4f39de4d8c7c1670c601134329937738b4dfa6f8ad', '0x14fb45dd869208edffcb221add152a20292283be172ddb6ccfd2d73e3710b6f4']
  condition: selection_behavior or selection_known_tx
falsepositives:
  - Protocol-owned rebalancing bots batching many small swaps (rarely GIVEN_OUT on a pool's own BPT)
  - Security researchers replaying the public exploit on a fork or testnet mirrored into the index
level: critical

```

**Known false positives:**

- Protocol-owned rebalancing or arbitrage bots batching many small swaps through the Vault
- Security researchers replaying the public exploit template on forks or mirrored testnets

**Tuning notes:** Thresholds (30 swaps, 10 dust swaps, amount_out<=100) come from the reported 65 micro-swap batch with amounts such as 17; lower swap_count to 20 for forks and raise the dust cut-off for 6-decimal tokens. Allowlist known keeper/arbitrage tx_from addresses. Requires an ETL that decodes Vault Swap events per transaction (chain, tx_hash, tx_from, call_name, swap_kind, pool_id, token_in, token_out, amount_in, amount_out); rename fields to your schema.

### 8 more detection rules (Splunk SPL, Microsoft KQL, Sigma), unlock with TL Creds

- Balancer v2 BatchSwap Borrowing Pool BPT Through Internal Balances - Sentinel (Exploitation) (locked: unlock with TL Creds)
- Balancer v2 Vault Internal Balance Staging Followed By Withdrawal - Sentinel (Post-Exploitation) (locked: unlock with TL Creds)
- Tornado Cash Funded Account Deploys Contract Then Runs Balancer BatchSwap - Sentinel (C2 / Staging Chain) (locked: unlock with TL Creds)
- Balancer v2 Fork Vault BatchSwap Dust Burst (Copycat Exploitation) - Sigma (locked: unlock with TL Creds)
- Balancer Vault Outflow To Attacker Addresses Or Fresh EOA Multi-Token Drain - Sigma (Post-Exploitation) (locked: unlock with TL Creds)
- Balancer v2 Exploit Contract Deployment Followed By BatchSwap Burst - Sigma (C2 / Multi-Chain Infrastructure) (locked: unlock with TL Creds)
- Balancer v2 Stable Pool Token Balance Drained To Dust And BPT Rate Suppressed (Post-Exploitation) (locked: unlock with TL Creds)
- Balancer v2 Exploit Attacker Infrastructure And Multi-Chain Vault Abuse (C2 / Fund Movement) (locked: unlock with TL Creds)

Sign up free to unlock the detection rules: https://threadlinqs.com/?modal=signup

## Sources

- [Understanding the Balancer v2 Exploit (OpenZeppelin)](https://www.openzeppelin.com/news/understanding-the-balancer-v2-exploit)
- [How an Attacker Drained $128M from Balancer Through Rounding Error Exploitation (Check Point Research)](https://research.checkpoint.com/2025/how-an-attacker-drained-128m-from-balancer-through-rounding-error-exploitation/)
- [Balancer - Rekt II](https://rekt.news/balancer-rekt2)
- [Balancer post-mortem report on $116M hack (Cointelegraph)](https://cointelegraph.com/news/balancer-post-mortem-report-116m-hack)
- [DeFi Protocol Balancer Starts Recovering Funds Stolen in $128 Million Heist (SecurityWeek)](https://www.securityweek.com/defi-protocol-balancer-starts-recovering-funds-stolen-in-128-million-heist/)
- [Nov 3 Exploit Post-Mortem (Balancer, Medium)](https://medium.com/balancer-protocol/nov-3-exploit-post-mortem-51dcbeb6b020)
- [Balancer exploit transaction 0x6ed07db1... (Etherscan)](https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742)

## Full data

The other 8 detection rules (Splunk SPL / Microsoft KQL / Sigma) unlock with TL Creds; a free account includes daily creds: https://threadlinqs.com/?modal=signup
Indicator values unlock with TL Creds, at a lower price on Red and Purple; AI agents can query rules and IOCs through the Threadlinqs MCP server (Purple): https://intel.threadlinqs.com/mcp

Canonical: https://intel.threadlinqs.com/threat/TL-2026-5075
