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);
}