Daniel Von Fange
An attacker stole approximately $11 million dollars of funds from BearnFi due to a bug in a BearnFi strategy contract which treated tokens of different monatary value as being equal to each other.
The BearnFi Bank contract used BUSD amounts for deposits and withdrawals and passed these BUSD amounts to BearnFi Alpaca Strategy for deposits and withdraws. The Strategy contract then used these amounts unchanged with the more valuable ibBUSD tokens.
Even though larger sums were being withdrawn from and deposited to the underlying investment platform, the actual deposits withdrawals to the users were still for the approximately correct amount in amount BUSD, since everything that the bank contract did was in BUSD.. However, a bug in the in strategy's internal accounting process meant that the the extra funds being deposited and withdrawn counted as an increase in yield for the entire strategy.
By repeatedly depositing and withdrawing 7-8 million dollars, the attacker was able to slightly increase their balance each loop from the several hundred thousdand dollars of extra yield that the strategy contract thought it was earning.
Let's walk through a typical attack loop:
- 7,887,636 BUSD is deposited by the attacker into the BearnFi Bank, which transfers it to the BearnFi Strategy.
- The Strategy takes all the BUSD that it has, and invests it in Alpaca. Because the Strategy contains left over BUSD from the attacker's previous loop, the actual amount sent to Alpaca is 8,101,666 BUSD. (Later, we'll get to how this extra amount got there.) The difference between the amount deposited by the user and the amount invested by the strategy is counted as an extra yield of 214,030 BUSD earned by the strategy, and is distributed to all users of the BearnFi strategy.
- The strategy then takes 7,887,636 ibUSD and places it in a staking contract. Note that even though the strategy is working with the more valuable ibUSD, it's still using the BUSD amount from the deposit.
- The attacker now requests a withdrawal of $7,901,623, which is now higher than the amount deposited since the Strategy thinks it has earned yield.
- The Strategy incorrectly uses the BUSD amount of the withdraw and withdraws $7,901,623 ibBUSD from the staking contract. This is exchanged into $8,116,032 BUSD.
- The Strategy now returns the requested $7,901,623 BUSD to the Bank and from there to attacker. The extra $214,409 BUSD from this process remains in the Strategy contract, and will be incorrectly counted as yield on the next loop.
The primary cause was that the strategy contract used two currencies with differing values without converting exchange rates between them. It assumed that they were 1:1 with each other.
Secondly, the errors in accounting which resulted were hidden because the BearnFi contracts explicitly checked to see if they didn't have enough money do an operation, and if they didn't have enough then they silently switched to using the actual amount they had rather than throwing an error when the actual amounts did not match what the accounting said. This meant that the constant internal math errors and incorrect accounting did not stop the system from apparently working.
Thirdly, the design of the totaly value calculation in the strategy was not resilient to errors. Rather than computing the actual total value owned by the strategy, the total was computed by tracking how much the strategy thought that it put in and took out. This made errors elsewhere in the contract be create apparent yield increases.
Unit testing the strategy, or even looking at the ERC20 transfer logs from a deposit or withdraw would would have caught these errors.
No.
We calculate the total value that backs OUSD by checking the actual live amounts controlled by each strategy. This means that there is no internal balance on OUSD strategies that can have incorrect accounting, and incorrect amounts transferred can't create false yield. A strategy withdrawing more than required, or depositing more than a user deposited does not alter our totals.
I've checked that our strategy code correctly handles exchange rates to the underlying investments. Two of our strategies (Compound, Curve 3Pool) use underlying investment tokens that trade at a different exchange rate than the stablecoins they can be exchanged form and our contracts calculate the exchange rates on these. One of our strategies (AAVE) uses a rebasing token at trades at 1:1 for the underlying stablecoin.
I've checked our OUSD contracts and we do not check a balance and them make a smaller transfer or reduce the amount stored in accounting, if we do not have as much funds as we should.
I've also manually verified from a withdraw transaction log that we with withdraw the correct amounts of the underlying token from strategies.
Attack transaction: https://bscscan.com/tx/0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c
BearnFi Bank Contract: https://bscscan.com/address/0xc60c6854d10c034718aca198fe92d73eb83b744c#code
BearnFi Vault Contract: https://bscscan.com/address/0x21125d94cfe886e7179c8d2fe8c1ea8d57c73e0e#code