Balancer Labs Incident — When Bug Bounties Fail
On June 28 in Ethereum block #10355807, a hacker drained over $500k in user funds from multiple Balancer Labs pools containing Statera and STONK erc20 tokens. The technical details of the attack are detailed by 1 Inch Exchange here and Balancer Labs itself here.
Smart contract hacks in Ethereum’s emerging DeFi space have been somewhat regular occurrences. However, this one could have been avoided… I submitted this exact attack vector to Balancer Labs’ Bug Bounty program 53 days earlier. At the time, only $250 of user funds were at risk.
Below I’ve included the full, unedited bug report that I submitted to Balancer Labs on May 6 as I mentioned I would on my Twitter yesterday.
The attack vector is quite simple. Balancer pools were not designed with deflationary tokens (like Statera and STONK) in mind. Specifically, these kinds of tokens include a transferFee that are assessed whenever transfer() or transferFrom() functions are called to move funds. For example, transferring 100 Statera tokens into a Balancer pool would result in only 99 tokens being added to the pool since 1 token would be burned in the process.
The key difference between Balancer and Uniswap, which handles these tokens correctly, is that a Balancer Pool contract does not double check its actual token balance before performing a swap. Instead, it assumes a successful transferFrom() call with 100 erc20 tokens will result in its token balance increasing by that exact amount, 100 tokens, and stores this value in a storage variable called _records[address]. This causes _records[address] to be inaccurate when dealing with deflationary tokens.
Balancer pools also include a function called gulp() which can be called to update the stored token balance in _records[address] to the actual value. This function was intended to be used for inflationary tokens but actually represents an attack vector when used with deflationary tokens.
As described in my original report, a malicious user could take advantage of this by:
- Identifying any pools with a deflationary erc20 token
- Repeatedly joining/exiting/swapping that deflationary token into the pool, increasing the divergence between the stored token balance (_records[address], which didn’t account for transfer fees) and actual token balance until actual token balance was near zero.
- Calling gulp() to update the pool’s stored token balance to the real, near-zero value. This massively inflates the price of the deflationary erc20 token in the pool.
- Draining the pool by now selling trivial amounts of the deflationary token for all other assets in the pool
I am publicly disclosing my bug bounty report now because I believe in DeFi and want to see it succeed. For that to happen, we need MORE focus on security. We need ROBUST bug bounty programs. And we need to aggressively acknowledge bugs and work to fix them BEFORE they are exploited.
Today, Balancer Labs announced they would cover all user losses in this hack and would pay out the highest-level bug bounty for my submission on May 6. Big thanks to the team for making the right decision in the end here.
Full, unedited Bug Bounty Report submitted on May 6, 2020
Hi Balancer Team,
Today I found a critical bug in the balancer pool smart contract that would allow a malicious user to steal all assets from a pool in certain situations
If pool contains an ERC-20 that includes a transfer-fee (e.g. DGX), the pool adds tokenAmountIn to storage variable _records[address(tokenIn)], but the pool will actually receive tokenAmountIn — erc20TransferFee
This allows for the pool to actually contain fewer assets than it stores in _records[address(tokenIn)]
A malicious user could use this to steal funds by doing the following:
- Repeatedly joining/exiting/swapping a pool with an erc20 like DGX
- Driving the actual balance of DGX in the pool to near-zero while the _records[DGX].balance is much higher
- Call Gulp(DGX), updating the pool’s balance to the real, near-zero value
- Draining another asset in the pool by swapping a trivial amount of DGX
- Repeating to drain every non-DGX asset in the pool
The key issue here is that it’s possible for Gulp() to decrease the value in the pool. This is actually happening today
- Pool: 0x2dbd24322757d2e28de4230b1ca5b88e49a76979
- The pool includes DGX and over time the actual balance of DGX in the pool is lower than storage balance due to transfer fees
- _records[DGX].balance = 0.559861212
- actual DGX balance = 0.105918779
- If someone did many more DGX swaps in/out, the actual DGX balance would continue decreasing to near-zero and all other funds in this pool will be at risk ($250 at the current moment)
Let me know if you need any more info from me
Founder & CEO, Hex Capital