today i go over a vulnerability for $BAL that could result on DoS by emptying double entry-point ERC20 tokens through flashloans.
This post is adapted from the bugfix review i wrote for immunefi.
Automated market makers (AMMs) are smart contracts that enable automated management of crowdsourced liquidity pools (LP), providing tradable tokens to decentralized exchanges (DEX). AMMs are an important primitive in DeFi, as anyone can deposit their own tokens in an AMM LP, receiving a share of trading fees and the LP tokens in return.
In the traditional trading model, buyers and sellers use something called an order book, where they write down what they want to trade in a huge list with everyone’s trades. The behavior of the buyers and sellers controls the market rates in the order book.
AMMs, however, do not rely on humans to manage trades, which are controlled directly by algorithms. An AMM replaces the buy and sell orders in an order book market with a liquidity pool of two assets, both valued relative to each other. As one asset is traded for the other, the relative prices of the two assets shift, and the new market rate for both is re-calculated.
The majority of AMMs use the constant mean market maker equation (also known as the constant product equation) to define how liquidity pools behave:
Balance of token A * Balance of token B = Constant product
For example, in a pool with 2,000,000 tokens A and 1,000 tokens B, the constant product is 2,000,000,000. This inverse correlation then determines the trading price of the assets at any moment, as the price of one token increases when the price of the second token decreases:
Market price for token A = Balance of token / Balance of token A
The price curve is often utilized as a way to visualize this relation:
To find the swap price for the assets in a given pool (i.e., the amount of token A received when you exchange a certain quantity of token B), we calculate the balances for each token that are needed in the pool to keep the constant product unchanged after the swap:
Balance of token A - Quantity of token A received =
(Constant product) /
(Balance of token B + Quantity of token B paid)
The amount of token A received from this swap conveys the “buy price”, and it’s calculated from the relation above:
Quantity of token A received =
(Balance of token A * Quantity of token B paid) /
(Balance of token B + Quantity of token B paid)
Launched in 2018, Balancer introduced a protocol for configurable liquidity on the Ethereum blockchain and other EVM-compatible systems. With Balancer, users are able to create liquidity pools with up to eight different ERC20 tokens, in any ratio. These pools can be thought of as automatically rebalancing portfolios, providing traders with what can be called a decentralized index fund.
Balancer routes several pools together, allowing easy trades from one token in one pool to any other token in another pool. These multi-asset pools work using what is called weighted math, designed to allow swaps between any asset whether or not they have a price correlation. Balancer's price equation is a generalization of the constant product equation, accounting for multi-tokens and weightings that are not evenly split, so that prices are determined by the pools’ balances and weights.
Balancer’s core smart contract is called “The Vault'', and it controls and holds all tokens in each of the Balancer pools. The Vault’s architecture separates the token accounting and management from the pool logic, simplifying the pools’ contracts. In this architecture, Balancer’s Protocol Swap Fees are the percentage of swap fees collected by pools defined on a contract named ProtocolFeesCollector.sol. These fees include flashloan fees, and they are adjusted through governance by the Balancer’s DAO.
Flashloans are special smart contract operations that allow the borrowing of any available amount of assets (up to the total liquidity) without collateral. This is possible as long as the liquidity plus interests are returned within one block transaction. If the borrower is unable to repay the loan, the entire transaction is simply reverted. A common use of flashloans is on arbitrage algorithms, searching for profit by trading an asset on a first DEX, and then getting back the asset plus gains on other DEXs.
In the context of Balancer, a trader could borrow any amount of tokens available on The Vault to use on a flashloan. The logic for these types of transactions is specified in the FlashLoans.sol contract, more specifically in the flashLoan
function:
1. This function starts with straightforward initializations, ensuring that the length of the token array and the amount array match, and assigning the zero address to the previousToken variable (to ensure the tokens are sorted and unique):
2. The first loop over the tokens’ array is used to record the pre-balance of each token, calculate the loan fees, and transfer each amount of token to the receiver. Note the callback function on receiver, receiveFlashLoan
, right after the loop:
3. The second loop checks whether each token’s balance is equal or larger than their previous balance, then add any balance surplus as a fee in the receivedFeeAmount
variable:
4. This fee is then transferred to ProtocolFeesCollector
:
💡 Do you see a problem with how the flashloan function is implemented?
We are very close to understanding this vulnerability. There is just one last piece of information we need: double entry point ERC20 tokens.
When ERC20 tokens operate on a proxy pattern, users interact directly with the proxy contract when performing token transactions. The proxy then connects to a target contract that implements the underlying logic. For a more in-depth review of how proxies work, check out our Wormhole Uninitialized Proxy Bugfix Review.
A double entry point ERC20 token relies on an architecture on which both users and contracts can interact directly with the target contract. Since the proxy can be bypassed, these tokens have two entry points.
Examples of double entry point tokens that are traded in Balancer pools are the Synthetix tokens, for instance, SNX and sBTC. For context, Synthetix is a derivative liquidity protocol that powers the creation of synths (synthetic assets), which can be traded in a decentralized and permissionless manner.
We went over several important concepts, and we are now ready to go in-depth into this vulnerability found in the flashloan method we described above, which is part of Balancer’s Vault contract.
💡 What if an attacker tried to execute a Balancer flashloan for both entry points of the same token?
In this case, the flashloan repayment (the second loop) would be interpreted as an excess of token balance at the second entry point and sent as fees to The Vault.
2. We then craft an Exploit contract that:
a. implements the flashloan function from the IVault.sol
interface,
b. Implements the method receiveFlashLoan
, from The Vault’s IFlashLoanRecipient.sol
interface:
3. Back to our script, we deploy this contract, exploiting the flashloan method with the first token as the first entry point and the full balance of the vault, and the second token as the second entry point with zero balance:
4. If we take another look at the flashloan function, in the first loop, we would see that the previous balance for the first entry point (target) is tracked as the full balance (and transferred out). The balance of the second entry point (proxy) is tracked as 0:
5. In the second loop, the check of the previous balance for the first entry point (target) would still be the full balance. The vulnerability lies during the check of the second entry point (proxy). Its previous balance being zero and its post balance being the full balance result in the receivedFeeAmount
receiving a difference equal to the full balance:
6. The Vault’s _payFeeAmount
method would then send the full amount of the sBTC to the ProtocolFeesCollector
contract, draining the Vault entirely. The resulting state of the pool would cause failures for legit transactions such as swaps and pool exits, resulting in a DoS situation.
Since the Balancer governance controls access to the ProtocolFeeCollector
contract, an attacker would not be able to retrieve funds after sending them to this contract. However, as we learned above, exploiting such vulnerability could lead to a DoS to the pool’s resources.
Upon notification, the Balancer Labs team quickly deployed mitigations in the affected contracts, moving the tokens to the ProtocolFeeCollector
contract and then back to a restored Vault, resuming normal operations.