Summary
convertLPToBeans() and convertBeansToLP() check the minAmountOut requirement after converting to the underlying amount and the final result might be less than the original minimum amount.
Vulnerability Details
Let me explain with convertLPToBeans().
function convertLPToBeans(bytes memory convertData)
internal
returns (
address tokenOut,
address tokenIn,
uint256 amountOut,
uint256 amountIn
)
{
tokenOut = C.UNRIPE_BEAN;
tokenIn = C.UNRIPE_LP;
(uint256 lp, uint256 minBeans) = convertData.basicConvert();
uint256 minAmountOut = LibUnripe
.unripeToUnderlying(tokenOut, minBeans, IBean(C.UNRIPE_BEAN).totalSupply())
.mul(LibUnripe.percentLPRecapped())
.div(LibUnripe.percentBeansRecapped());
(
uint256 outUnderlyingAmount,
uint256 inUnderlyingAmount
) = LibWellConvert._wellRemoveLiquidityTowardsPeg(
LibUnripe.unripeToUnderlying(tokenIn, lp, IBean(C.UNRIPE_LP).totalSupply()),
minAmountOut,
LibBarnRaise.getBarnRaiseWell()
);
amountIn = LibUnripe.underlyingToUnripe(tokenIn, inUnderlyingAmount);
LibUnripe.removeUnderlying(tokenIn, inUnderlyingAmount);
IBean(tokenIn).burn(amountIn);
amountOut = LibUnripe
.underlyingToUnripe(tokenOut, outUnderlyingAmount)
.mul(LibUnripe.percentBeansRecapped())
.div(LibUnripe.percentLPRecapped());
LibUnripe.addUnderlying(tokenOut, outUnderlyingAmount);
IBean(tokenOut).mint(address(this), amountOut);
}
It converts minBeans to minAmountOut in underlying and uses it during the swap. After that, the final amountOut is calculated back from outUnderlyingAmount.
A rounding loss is expected while converting minBeans to minAmountOut, and outUnderlyingAmount to amountOut.
So if the swap result(outUnderlyingAmount) is exactly the same as minAmountOut, amountOut will be less than minBeans after the conversion.
Impact
In convertLPToBeans() and convertBeansToLP(), the slippage protection wouldn't work as intended.
Tools Used
Manual Review
Recommendations
We should validate the slippage again as a final step.
function convertLPToBeans(bytes memory convertData)
internal
returns (
address tokenOut,
address tokenIn,
uint256 amountOut,
uint256 amountIn
)
{
tokenOut = C.UNRIPE_BEAN;
tokenIn = C.UNRIPE_LP;
(uint256 lp, uint256 minBeans) = convertData.basicConvert();
uint256 minAmountOut = LibUnripe
.unripeToUnderlying(tokenOut, minBeans, IBean(C.UNRIPE_BEAN).totalSupply())
.mul(LibUnripe.percentLPRecapped())
.div(LibUnripe.percentBeansRecapped());
(
uint256 outUnderlyingAmount,
uint256 inUnderlyingAmount
) = LibWellConvert._wellRemoveLiquidityTowardsPeg(
LibUnripe.unripeToUnderlying(tokenIn, lp, IBean(C.UNRIPE_LP).totalSupply()),
minAmountOut,
LibBarnRaise.getBarnRaiseWell()
);
amountIn = LibUnripe.underlyingToUnripe(tokenIn, inUnderlyingAmount);
LibUnripe.removeUnderlying(tokenIn, inUnderlyingAmount);
IBean(tokenIn).burn(amountIn);
amountOut = LibUnripe
.underlyingToUnripe(tokenOut, outUnderlyingAmount)
.mul(LibUnripe.percentBeansRecapped())
.div(LibUnripe.percentLPRecapped());
+ require(amountOut >= minBeans, "too less Beans");
LibUnripe.addUnderlying(tokenOut, outUnderlyingAmount);
IBean(tokenOut).mint(address(this), amountOut);
}