đź“„

Report #21139

Report Date
June 5, 2023
Status
Closed
Payout

In `sop()` BEANS are exchanged for 3CVR LPs without slippage

‣
Report Info

Report ID

#21139

Target

https://etherscan.io/address/0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5

Report type

Smart Contract

Impacts

Theft of unclaimed yield

Has PoC?

Yes

Bug Description

The function sop() in the season facet mints beans and then exchanges some of the minted beans for 3CVR on the curve metapool via this call:

uint256 amountOut = C.curveMetapool().exchange(0, 1, sopBeans, 0);

The issue is that the specified minimum amount of LP to return from the exchanged beans is set to 0. An attacker can sandwich the transaction for profit and have the protocol receive less 3CVR as a consequence.

Impact

Exchanged 3CVR are supposed to be distributed to stalkholders, but the amount that will be distributed will be way lower than expected reducing their yield in favor of the attacker.

Risk Breakdown

At the current state of the protocol sop() is never called, but in case of a flood it's very easy to exploit and will be done 100% by MEV bots.

Recommendation

A fix is not trivial, because calculating a minimum amount of LP to receive and reverting in case the received amount is too low would DOS the entire protocol.

On the other hand, not fixing this bug means losing funds whenever sop() is called.

A solution might be to:

  1. Calculate the minimum amount of LP the protocol is willing to receive from the exchange
  2. Execute the exchange by specifying the minimum amount of LP in a try/catch block
  3. If the exchange execution reverts, store the amount of beans to be stored in a storage variable
  4. The next time sop() is called it will try to convert the current amount of beans plus the previous one
  5. Add a function that can be called manually to convert the beans to LP

I might be able to come up with a cleaner solution, maybe Halborn can help as well.

Proof of concept

An attacker is aware that the next time gm() is called it will call the function sop() and exchange 100.000 BEANS for 3CVR:

  1. The attacker swaps BEANS for 3CVR himself, so now the pool contains many BEANS and little 3CVR
  2. The attacker calls gm(), which exchanges the 100.000 BEANS for 3CVR, which will return little 3CVR
  3. The attacker swaps back the 3CVR for BEANS

By doing this the attacker earns a profit while stalkholders lose yield.

BIC Response

This is not a valid bug report due to the following reasons:

  • Beanstalk will sell Beans during a Flood based on the instantaneous deltaB, i.e., Beanstalk is only going to sell Beans if above peg, and if so, sell enough Beans to return to peg. Given this, any sandwich attack on the Flood (one where someone flash loan borrows Beans, sells Beans, calls gm(), and buys back the Beans at peg to pay back the loan in an attempt to make a profit) would simply result in fewer or zero Beans being sold (and thus less or no 3CRV being distributed to Stalkholders). This is not a theft of yield because the yield is not created at any point.
  • Given the above reasons, the PoC in the report doesn’t work.
  • There is the possibility of repeatedly performing this flash loan attack on the Flood every Season such that Beanstalk never sells any Beans. This is a known issue but would require the attacker to guarantee that they can call gm() each Season.

Due to these reasons, we are closing the submission and no reward will be issued.