Indexed Attack Post-Mortem
Today Indexed suffered its first hack since its deployment in December, and it was a pretty devastating one. About $16m worth of assets were stolen from the indices DEFI5 and CC10 by 0xba5ed1488be60ba2facc6b66c6d6f0befba22ebe.
Needless to say, we’re shocked and upset: hearing ‘we’re sorry’ from a protocol always seems to ring hollow in the aftermath of these incidents (especially to those impacted) but it bears repeating: we are truly apologetic, to both those who have had funds drained, and those who remain in unaffected pools.
It is important for us to let you know exactly what happened, as soon as possible, and the rest of this post lays that out in detail.
This attack exploited the way index pools are rebalanced. To explain what happened, we’ll need to dig into some fairly technical details about the protocol, and we’ll assume you’re familiar with Balancer and understand what an index fund is.
How index pools handle new assets
When a token is added to an index pool, we use approximate values with a Uniswap oracle to determine how to price the token within the Balancer pool. This is done to remove any need for the pool to interact with external markets in order to rebalance, and allows tokens to be traded into the AMM before the pool has any balance in them.
To do this, we use a function `extrapolatePoolValueFromToken`. This finds the first token in the pool with a target weight over 0 and which is fully initialized, then multiplies the pool’s balance by the reciprocal of its weight — so if the pool has 10 UNI at a weight of 10%, it’ll say the pool is worth 100 UNI. The controller uses this with a Uniswap oracle to determine the amount of a new token X that is worth 1% of the pool, which is then used to price swaps. Until the pool reaches that balance for the token, it will buy it at a slight premium; once it hits the balance, the token is considered “initialized” and can be both bought and sold by the pool.
Occasionally, token prices will change so quickly that the minimum balance is so far off of the value of 1% of the pool that no one is willing to swap it into the pool. To prevent this from causing a delay in a rebalance, the controller has another function updateMinimumBalance which resets the virtual balance for an uninitialized token.
If you’ve worked on contracts before, you probably see where this is going.
At the time the attack started, DEFI5 was ready for a re-index (anyone can trigger one after 3 re-weighs, which occur once a week). The first call in the transaction was to trigger a re-index of DEFI5. At this time, UNI was the first asset in the token list which was fully initialized and had a desired weight over zero, so the price of UNI was used to approximate the pool value and set the minimum balance for SUSHI. This set a reasonable minimum balance for SUSHI of 11,926, or about $126k.
Next, the exploit contract took out approximately $156m worth of flash swaps in UNI, AAVE, COMP, CRV, MKR, SNX (the initialized assets in DEFI5) from Sushiswap and Uniswap V2.
The contract then used all of the borrowed assets to purchase UNI from the pool in chunks, as the pool does not allow swaps to send more than 1/2 of the pool’s existing balance in a token or purchase more than 1/3 of the pool’s balance in a token. This took dozens of swaps, but they managed to dump the tokens into the pool.
The attacker then executed a minimum balance update on the controller. Because they had purchased nearly all of the UNI in the pool, its balance was very low when the controller queried it, and so the approximated value of the entire pool was calculated as 29,851 SUSHI (~$300k), despite the pool having received over a hundred million dollars worth of other assets.
The previously purchased UNI was then used to mint new DEFI5, again in chunks due to limitations on the relative size of a single-token mint. This resulted in the pool supply being inflated by orders of magnitude.
Next, the caller used the borrowed SUSHI to mint additional DEFI5 at the extremely inflated valuation caused by the minimum balance exploit, then burned the DEFI5 for all of the underlying assets, and repeated this a number of times.
Finally, they paid off the flash loans and made out with about $11m worth of assets.
The CC10 exploit was essentially the same thing, except that the initial re-index step had already been done.
The fix for the contract seems pretty straightforward in terms of preventing any future attacks against this mechanism. We will modify the controller smart contracts to remove the approximate value function and replace it with one that takes the combined value of the balances held by a pool in every token it owns. Additionally, the mere fact that it was possible to do both a re-index and a minimum balance update in the same transaction is — in retrospect — unsafe: it should have a minimum wait time of at least a day or two. A lot of Ethereum developers we respect have reached out offering help since the attack occurred, and we will seek out as much feedback on the new code as we can before submitting it for governance approval.
As for compensating people who lost funds, this is — so soon after the event — still up in the air. The core team will be discussing with the community how best to handle this situation (as well as talking with similarly affected protocols for insights into their own approaches), and we will hopefully have a proposal for governance soon. We realise that this is far from a concrete action plan, but we need to get our heads on straight first.