Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Bug Description
Beanstalk hold BEAN's as part of protocol functionality. It has convertFacet to maintain the peg of BEAN. with help of Curve and well liquidity it maintain BEAN to 1$ peg by rewarding users.
This convertFacet logic is vulnerable. it doesn't verify well address explicitly while converting. It run on assumptions that if token has deposit than it is whitelisted. but this can be bypassed by zero value bug.
How whitelist can be bypassed ?
convert function accept 3 param.
Bug :- It didn't validate that stems and amounts should not have zero length. [This is good to have resolution]
next it call LibConvert.convert with help convertData. convertData have convertKind and other helping data. if convertKind is WELL_LP_TO_BEANS, it has user provided well (token) address.
Bug :- It didn't verified well address is whitelisted and lp is greater than 0. [This well address must be validated by isWell or by checking s.ss[token].selector]
it do call to _wellRemoveLiquidityTowardsPeg, well address can be malicious and lpConverted is zero from previous call. so it return
(amountIn = 0) and amountOut = (maliciousWell Output).
lpToPeg(well) => this can be bypassed since well is malicious.
This goes back to convert function. _withdrawTokens is completely bypassed because length of stems and amounts is zero and fromAmount is also zero.
toToken = BEAN
fromToken = Malicious well
toAmount = can be anything because returned by Malicious well's removeLiquidityOneToken call. = BEAN balance of beanstalk's contract.
fromAmount = 0
so, eventually this convert function make BEAN deposit without withdrawing any real token.
Later this fund can be claimed by withdrawDeposits call to siloFacet.
Impact
=> BeanStalk contract holding all bean can be drained. which is 22,848,231 BEANS. => This hacked BEAN can be swapped in LP Pools. Current available Lp is
Well Pool = 6750 ETH, value of 12,765,569 USD.
Curve Pool = 105,017 Curve, value of 107,958 USD.
This is effective benefit of hacker.
Risk Breakdown
Difficulty to Exploit: Easy
Recommendation
Well address in convertFacet must be explicitly verified. This validation can be put at LibWellConvert's convertLPToBeans and convertBeansToLP function.
Proof of concept
POC code.
The hardhat based runable code is attached in mega link.
Based on our bounty page, this submission's (Smart Contract - Critical) reward is capped at the lower of (a) 10% of practicable economic damage, or (b) USD 1 100 000, primarily taking into consideration Beans/BDV at risk and paid at the rate of 1 BEAN to 1 USD.
The BIC determined that the funds at risk were all of the Beans in the Beanstalk contract (~22.8M) given that an attacker could have Converted all of these Beans into their own Bean Deposits (which could then be Withdrawn and sold).
Given this, the BIC has determined that this report qualifies for the max reward of 1.1M Beans.
function _wellRemoveLiquidityTowardsPeg(
uint256 lp,
uint256 minBeans,
address well
) internal returns (uint256 beans, uint256 lpConverted) {
uint256 maxLp = lpToPeg(well);
require(maxLp > 0, "Convert: P must be < 1.");
lpConverted = lp > maxLp ? maxLp : lp;
beans = IWell(well).removeLiquidityOneToken(
lpConverted,
C.bean(),
minBeans,
address(this),
block.timestamp
);
}