Exploring a faulty oracle impacting LPs on Balancer

Antoine Bellanger
SwissBorg Engineering
5 min readAug 10, 2023

Introduction

In March 2023, SwissBorg offered a new ETH strategy providing liquidity in the wstETH/wETH pool on Balancer (Polygon).

In June 2023, after unwinding the position, the DeFi team at SwissBorg stumbled upon something that looked problematic.

As the issue was recently fixed by Balancer Labs, here are our detailed findings and the remediations put in place.

Bug

While analysing liquidity in the wstETH/wETH pool, we realized that the liquidity providers were gradually losing money regardless of impermanent loss (IL).

The B-stETH-BPT pool is a ComposableStablePool whose code can be viewed on Polygonscan. The contract has a method named getRate(), representing the value of 1 IOU. The rate is defined in the comments of the contract as “a monotonically increasing function”.

After plotting historical data, we realized that this requirement was violated multiple times over the lifespan of the pool:

The “getRate()” method over time for the wstETH/wETH pool.

Further analysis

From the plot displayed above, we identified a couple of blocs where the rate of the pool suddenly dropped.

Event #1:

The drop is visible in the following chart:

The drop in “getRate()” during the Event #1.

Event #2:

The results are displayed on the next graph:

The drop in “getRate()” during the Event #2.

From both these events and the six transactions, we’re able to find a couple of patterns:

  • In all of the transactions in the blocks around where the drop happens, wstETH is removed from the pool.
  • Putting aside Tx #2.2, all the transactions show that the users are buying wETH with wstETH on a DEX (Kyber, Uniswap) and then are doing the swap back (wstETH to wETH) through the Balancer pool.
  • These events happened during periods of increased volatility for ETH, as highlighted in the next two charts. (The red dotted bar indicates the time when the pool rate dropped).
ETH/USD price chart around the Event #1.
ETH/USD price chart around the Event #2.

From these observations, we analyzed further the logic of this contract, guessing that it could be a problem with an oracle.

ComposableStablePools are based on the Curve implementation of the StableSwap. They allow swapping of both pegged and highly correlated assets, such as wstETH and wETH, with low slippage. To account for rates that differ from 1:1 during swaps, the pool scales token balances by rates obtained from “rate provider” contracts. These are configured during pool deployment, and their values are cached within the pool. The pool owner can set the duration of the cache, and anyone can refresh the rates on demand.

Therefore, we can see that the contract relies on a ChainlinkRateProvider, which in turn relies on a Chainlink oracle to provide a price for the wstETH/wETH pair. This Chainlink oracle is not reported as an “official” Chainlink oracle as it can’t be found on their website reporting all the oracles they maintain. Furthermore, the oracle is quite stale, at the time of writing this paper (June 15th), the oracle has not been updated for 21 hours, and the price is slightly off: 1.1278 (oracle) vs. 1.1285 (Lido). The pool was configured with a 15-minute cache duration, so the price it uses can lag Chainlink by an additional 15 minutes.

Before performing a swap, joining or exiting the pool, the pool will check if the cache is still valid or needs to be refreshed. It then calculates the price of the wstETH in ETH to have both components priced in the same asset (ETH).

In case of a price movement onchain moving the wstETH / ETH price, the oracle will be lagging, and the cache on the pool will increase this effect. Note that the cache can be forced to be refreshed by a user, but this doesn’t happen automatically.

During periods of market volatility, this oracle latency can cause the wstETH price in the Balancer pool to differ from the external price, resulting in a profitable arbitrage opportunity.

For instance, a user could buy ETH with wstETH on the open market, then trade it for cheap wstETH in the pool, to the detriment of LPs.

Impact on other pools

Finally, we ran some extra checks to see whether other Polygon pools were impacted, and found that the new wstETH pool (wstETH/Boosted Aave V3 WETH) was also impacted, as can be seen on the following chart:

The “getRate()” method over time for the wstETH/Boosted Aave v3 wETH pool.

Mitigation

After discussing this issue with Balancer Labs, the following mitigations were put in place:

The documentation of the ComposableStablePool was updated to reflect a more nuanced understanding of the behaviour of getRate: in particular, that users should not assume it is monotonically increasing.

  • The oracle for the wstETH rate should be upgraded to the new Chainlink wstETH / stETH Exchange Rate Oracle.
  • The two impacted pools incentives were migrated to a new pool implementing the updated oracle mentioned above.

Conclusion

In conclusion, we thank Balancer Labs for their work in fixing this issue and their valuable comments on this article. We were glad to assist them with the analysis of the new oracle.

While the issue was reported through Immunefi and fixed by Balancer Labs, it didn’t qualify for a bug bounty as the team considers that “LPs are required to consent to the dependency of rate providers for boosted pools”.

Finally, this issue impacted SwissBorg with a small loss (0.52 ETH, ~0.18% of the funds deployed in this strategy). SwissBorg fully compensated the users participating in the strategy. I also thank my colleagues Frédéric Faye and Nicolas Rémond for their help in investigating this issue.

--

--