Rate Provider manipulation in Balancer Boosted Pools scope expanded

Juani
Balancer Protocol
Published in
8 min readSep 14, 2023

Overview

Balancer Labs received a report from GothicShanon89238, through the Immunefi platform, that some types of rate providers could be manipulated outside of pool joins or exits (e.g., through direct “donation” of main tokens), with consequences for certain Balancer pools. GothicShanon89238 was awarded a bounty of $130k USD for responsible disclosure, submission of the report, and helping to define the final scope of the vulnerability (which involved multiple proof-of-concept iterations).

This potential vulnerability was never exploited, and no funds are at risk.

We also received a report from SwissBorg about a specific instance of a faulty rate provider enabling arbitrage against certain Boosted Pools, leading to an unexpected drop in the overall rate.

We found that when these rate providers were paired with constituent tokens of Composable Stable Pools (e.g., the AAVE V3 wrappers in bb-a-USD), constituent token rates could be manipulated and used to violate certain assumptions made by the pool during the calculation of protocol fees.

The final result would be inflated protocol fee payments, and manipulation of the overall rate of the Composable Stable Pool. Excess protocol fees can always be refunded. However, manipulation of a Composable Stable Pool rate (e.g., bb-a-USD) would enable arbitrage within Balancer against any pool containing that BPT as a token: especially another stable pool.

Furthermore, any manipulation would also affect external protocols using those BPT rates in some fashion (e.g., BPT as collateral).

(BPT is an abbreviation for the LP token that users receive for providing liquidity to a given pool.)

Context

Methodology

Composable Stable Pools pay protocol fees in BPT, which are minted to the Vault’s ProtocolFeesCollector. This protocol debt must be paid before each join or exit: otherwise, someone could exit the pool and not pay their share of the debt, or join it and buy some debt not accrued by them.

These fees are paid in the _beforeJoinExit hook of ComposableStablePool, which calls _payProtocolFeesBeforeJoinExit, and ultimately _getProtocolPoolOwnershipPercentage in the ComposableStablePoolProtocolFees contract. This is where the problem lies.

The pool ownership function calculates the amount of BPT the protocol should mint to inflate the pool supply just enough to account for protocol fees accumulated since the previous join or exit. Currently, protocol fees are 50%, so if a pool grew by 10% (as measured by its invariant), the protocol should grow the total supply by 5%: 50% of the 10% organic pool growth due to yield and swap fees.

Since computing and paying protocol fees on every swap would be prohibitively expensive, we do this only on joins and exits: whenever liquidity is added or removed. We measure the total growth at every join and exit by comparing it to the total invariant at the last join or exit. And here’s where things get tricky!

Fee Calculation (Swap vs. Yield)

The first thing to understand is how token rates are incorporated into pool calculations, when a token has an associated rate provider. A rate provider contract, through its getRate function, converts between the “wrapped” pool token and an underlying token whose value it tracks (e.g., the exchange rate between aDAI and DAI).

Within a pool, these rates are multipliers, which together with the token decimal scaling factors yield the final 18-decimal floating point balances used in the invariant calculations. Positive rates increase the effective balance of a token (relative to the actual number of tokens in the Vault), and negative rates decrease it.

There are two sources of invariant growth that affect the total value of the pool: swap fees, and yield fees. These fee percentages can be different, so we need to separate the growth due to swaps from that due to yield (i.e., to the underlying token’s rate increasing).

The problem is that they are not really independent. Protocol fees accumulate during “gaps” between successive joins and exits (see below). The token yield rates are external to Balancer, and can change at any time. Swaps can likewise happen at any time. If rates change while swaps are happening, that affects the amount collected in swap fees (e.g., if a token rate goes up, the swap fees collected will be slightly higher, as the apparent token balance will be inflated by the rate).

To separate these, we introduce an approximation: the calculations assume all swaps happened immediately after the last join/exit, and all rate changes occurred immediately before the current transaction.

Rate change approximation

We store the “old” token rates (the prevailing rates at the last join/exit), so that we can compute an invariant assuming zero yield growth, which allows us to isolate the effect of swap fees.

In a similar manner, we store the “old” amplification factor, as any changes there during the “gap” would likewise alter the invariant and invalidate the growth calculations.

Yield Fee Exemption in Nested Pools

And there is one final twist regarding yield protocol fees. Since pools can be nested, we’d like to avoid “double dipping,” i.e., charging fees more than once on the same yield. For example, if protocol fees are charged on the yield of bb-a-USD (the Aave-boosted “3-pool” of USDC/USDT/DAI), then yield fees should not be charged on the bb-a-USD portion of a nested pool, such as wstETH/bb-a-USD.

To achieve that, in addition to calculating separate invariants to isolate yield fees from swap fees, we similarly isolate “exempt” from “non-exempt” yield. Here’s an overview, taken from the code documentation of _getProtocolPoolOwnershipPercentage, of the different invariants and how they stack up under normal conditions. (For simplicity, this shows the “join” case, where the invariant increases; with an exit it would decrease, but the same math applies.)

Invariant relationships

These invariants should always obey the following inequality:

lastPostJoinExitInvariant <= swapFeeGrowthInvariant <= totalNonExemptGrowthInvariant <= totalGrowthInvariant

We recognized from the beginning that the behavior of rate providers — which are, after all, necessarily dependent on the tokenomics of the underlying tokens — might behave differently, and that great care must be taken in “pairing” tokens and rate providers such that they are compatible with each other and the pool they’re in (or nested within). Some rate providers ensure the rate increases monotonically; others do not.

We enumerated our assumptions, and stated that there could be pathological situations where they might not hold. But we were not aware of any at that time, and it was thought that the complexity of attempting to account for all possible cases outweighed the potential benefits. From the same documentation:

Ideally, rate providers used in AMMs should be monotonically increasing, non-volatile, and non-manipulable, such that any rate changes reflect true, organic shifts in the underlying token value. These are mostly valid assumptions. Mostly.

Vulnerability Brief

Presented with a concrete example of how rate manipulation is possible, we found that the root of the vulnerability in the Composable Stable Pool protocol fee calculation was incomplete enforcement of the invariant bounds.

Essentially, manipulating the rate outside the join/exit context (e.g., through direct token donation to artificially raise the rate) can cause the assumptions behind the approximation to fail, causing the pool to misinterpret a “fake” increase in yield fees as a “real” increase in swap fees, and therefore grossly miscalculate the protocol fees.

Specifically, to calculate the amounts of BPT the pool should mint to the protocol, it checks that swapFeeGrowthInvariant > lastPostJoinExitInvariant and zeroes out swapFeeGrowthInvariantDelta if the value declines: but fails to do this check to calculate nonExemptYieldGrowthInvariantDelta

This allows the protocol yield percentage to be artificially inflated, leading to overpayment of protocol fees, decreasing the overall rate, and potentially presenting arbitrage opportunities.

Affected Contracts

Composable Stable Pool (all versions)

Mitigation

As soon as we learned of the issue, the Emergency SubDAO acted to place all Composable Stable Pools in Recovery Mode, which turns off protocol fee collection entirely, thereby skipping the problematic code and eliminating the vulnerability. Balancer’s Twitter account then disclosed this action.

When we believed we fully understood the issue, after much appreciated help from Certora, it was apparent that Composable Stable Pools were only affected if they 1) used rate providers; and 2) the rate providers allowed the rate to be artificially affected in some fashion (e.g., by direct “donation”: sending tokens directly to the wrapper contract).

After careful analysis and review by the Integrations team at Orb, Balancer Labs has now reactivated protocol fees where possible (see the list below). We also have a permanent fix for this vulnerability, which involves two steps:

  1. Redeployment of the Composable Stable Pool (V5) with the following fixes:
  • Complete enforcement of the “invariant bounds,” such that any attempted manipulation of rates results in zero protocol fees: and no change to the overall rate
  • Simplification of the “exempt yield fee” logic, to prevent even more obscure exploits that might arise from “mixing” exempt and non-exempt tokens. In V5, there is a single exemption flag that applies to all tokens.

2. Migration of affected pools to the new version

  • As part of the migration, gauges (and therefore incentives) for the affected pools were killed (per governance vote), or were scheduled for the next epoch
  • Replacement pools were deployed with “donation-resistant” wrappers from BDG Labs
  • Some weighted pools with potentially manipulable rate providers were also migrated

The last measure (replacement of wrappers) was done out of an abundance of caution, as we are confident that the new Composable Stable Pool completely mitigates this vulnerability within Balancer. However, as noted above, consumers of that BPT (e.g., BPT as collateral) might still be vulnerable to some degree with the old wrappers, so we will reduce that risk with the new deployments.

Conclusion and Future Plans

Balancer Labs believes the mitigation described above completely and permanently resolves this issue for Composable Stable Pools, and all pools that were redeployed with BDG donation-resistant wrappers.

Nevertheless, it is important to be very careful when choosing rate providers and pairing them with specific yield tokens, to ensure compatibility and resistance to manipulation: especially in today’s world of ever-increasing composability.

Balancer is permissionless, and cannot monitor or prevent manipulable tokens from being introduced to the platform. Even the most carefully designed lending solution is only as strong as its weakest token. Some donation-susceptible wrappers are still in use, and individual wrappers may have further unknown vulnerabilities.

We believe that the current pools, deployed with donation-resistant wrappers and the patched Composable Stable Pool, are safe from this particular issue in their current configurations. Yet the burden remains on integrators building new products to ensure that none of the underlying tokens at any level are manipulable.

--

--