Balancer V2 - Token frontrun vulnerability disclosure

This article describes a live vulnerability in the Balancer V2 Vault only, which has been a known issue to the Balancer team since March 2023 and has never been exploited up to this date. Funds currently deposited in the V2 Vault are safe, and no action from LPs is needed; the end goal of this article is to disclose the problem to the public domain for transparency. Since this discovery, we have been and continue to monitor and proactively prevent any exploits (see “Mitigation” below).

tl;dr; Funds are safe. The problem only (potentially) affects token addresses that are not live onchain today and would eventually be deposited in the Balancer V2 Vault.

Description

Background

As is often the case with vulnerabilities, this issue arises from a gas optimization in combination with an unrelated feature.

Balancer V2’s Vault was designed to be flexible and future proof, trying to cover multiple use cases and serving as a platform. One of the features it included was internal balances: a sort of internal “wallet for users” that allows it to store tokens on someone else’s behalf. This was useful to make gas efficient trades: a trader could store their tokens in the Vault, and use those funds to trade against pools registered in the V2 Vault without any token transfers. There were also other use cases for this feature, such as partial recovery of funds in case token transfers failed; see previous Euler’s exploit post mortem. This feature has worked well, and there are no known issues with internal balances when used as intended.

The gas optimization in question is related to safeTransfer , which ends up calling this code in Balancer V2 Vault:

function _callOptionalReturn(address token, bytes memory data) private {
    // We need to perform a low level call here, to bypass Solidity's return
    // data size checking mechanism, since we're implementing it ourselves.
    // solhint-disable-next-line avoid-low-level-calls
    (bool success, bytes memory returndata) = token.call(data);

    // If the low-level call didn't succeed we return whatever was returned.
    // solhint-disable-next-line no-inline-assembly
    assembly {
        if eq(success, 0) {
            returndatacopy(0, 0, returndatasize())
            revert(0, returndatasize())
        }
    }

    // Finally we check the returndata size is either zero or true - note that
    // this check will always pass for EOAs
    _require(
        returndata.length == 0 || abi.decode(returndata, (bool)),
        Errors.SAFE_ERC20_CALL_FAILED
    );
}

This code was taken from OpenZeppelin’s implementation of the transfer functions in SafeERC20, which is pretty much the same as the original except for one detail: it doesn’t check whether the target token address has code or not. This small gas optimization is not an issue under normal circumstances, as tokens do have code and the transfers succeed.

The problem

What happens if we transfer tokens to the Vault’s internal balance for tokens that don’t yet exist onchain? The original code would revert if you attempted to transfer a token that didn’t exist: but since that check was removed, the transfer succeeds, and the internal balance will be recorded by the Vault. In other words,alice can transfer an arbitrary balance of nonexistent tokens to her internal balance wallet in the Vault.

That by itself is not a problem: by definition, owning nonexistent tokens is not an issue.

The real problem arises if a malicious actor knows a token address that doesn’t exist onchain in the present, but will exist and become a valid asset in the future. An attacker can set internal balances to any arbitrary value they want before the token is live onchain, and if that token ever ends up in a Balancer pool, the attacker will have arbitrary (fake) value to use in swaps against whatever value the token is paired against.

In summary:

  • An attacker knows a token address that will be launched onchain in the future, and sets the internal balance for that token to an arbitrary value in the vault
  • The token is launched onchain
  • A pool is created in Balancer V2, pairing the token launched against other tokens (e.g. USDC, WETH, etc).
  • The attacker can now use that fake internal balance to trade against that pool, effectively draining value from the pool at the LPs’ expense.

Thankfully, the scope of the problem is limited:

  • Tokens that already exist and haven’t been exploited this way are not subject to this attack. If an attacker tries to set internal balances with a token that is live onchain, this will not work.
  • Therefore, this can only be an issue for tokens that haven’t been launched yet.

Mitigation

Balancer V2 is immutable, so there is nothing to be done at the contract level.

We can, however, monitor the vault for attacks like this before we add liquidity to a pool.

The frontend implements an allowlist that needs to be manually approved before the pool appears publicly. The team at Balancer runs scripts internally that check whether this issue has been exploited for new tokens before seeding pools with meaningful liquidity, and before adding them to the frontend. This way we can keep the situation under control, even with the bug still live.

For new pool / token launches, we now have Balancer V3. As stated already, there is no need to migrate existing liquidity out of the V2 vault given that existing tokens can’t be affected by this problem.

We will continue to monitor the situation, but will also increasingly lean on V3 for new pool launches now that it’s available. Balancer V3 is not affected: it does not implement internal balances, and does not use modified libraries.

The mitigation has been in place since shortly after the first report reached Balancer’s team. Since the issue did not affect existing funds, and monitoring was already in place, we decided to postpone the disclosure until we found a better solution.

As stated above, since Balancer V2’s Vault is immutable, the solution was to develop a new Vault. This was one of the catalysts (among others) that encouraged us to start building V3 in the first place.

Now that Balancer V3 is live, new pools can be created on the new platform, also leveraging new features and enhanced developer UX. Pools can still be created in Balancer V2 with new tokens, as monitoring will continue.

Acknowledgement

The issue was first reported by Kankodu via Immunefi on March 4th, 2023. A bounty was paid to the whitehat, according to the rules of Balancer’s bug bounty program.

The bug was also reported by k_besic on August 21st, 2023, and by Holterhus on October 23rd, 2023. All three whitehat reports accurately described the issue. Unfortunately, it was still too early for a public disclosure at the time.

7 Likes