Incorrect swap logic in _protocolWithdraw
Description
The _protocolWithdraw
function in MorphoVaultStrategy contains two issues in its background swap logic:
function _protocolWithdraw(uint256 assets_, uint256) internal virtual override {
// [...]
if (isBackgroundSwapEnabled) {
uint256 expectedOutput = UniswapV3HelperV1.getExpectedOutput(
IQuoterV2(uniswapQuoter), _asset, address(_backgroundSwap), assets_, poolFee
);
isValidQuote(_asset, address(_backgroundSwap), assets_, expectedOutput);
//@dev add slippage protection and increase the amount to withdraw by 1%
uint256 swapAssets = expectedOutput.mulDiv(100_00 + MAX_SLIPPAGE, 100_00, Math.Rounding.Floor);
uint256 _totalAssets_ = _morphoVault.convertToAssets(_morphoVault.balanceOf(address(this)));
uint256 assetsToWithdraw = swapAssets > _totalAssets_ ? _totalAssets_ : swapAssets;
//slither-disable-next-line unused-return
_morphoVault.withdraw(assetsToWithdraw, address(this), address(this));
uint256 swappedAmount = _swapExactTokenToToken(address(_backgroundSwap), _asset, assetsToWithdraw, 0);
emit BackgroundSwapWithdraw(assetsToWithdraw, swappedAmount);
} else {
// [...]
}
}
First, UniswapV3HelperV1.getExpectedOutput
calls quoterV2.quoteExactInputSingle
to calculate output tokens (_backgroundSwap
) received for assets_
input tokens (_asset
):
function getExpectedOutput(IQuoterV2 quoterV2, address tokenIn, address tokenOut, uint256 amountIn, uint24 poolFee)
external
returns (uint256 amountOut)
{
IQuoterV2.QuoteExactInputSingleParams memory quoteExactInputSingleParams = IQuoterV2.QuoteExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: amountIn,
fee: poolFee,
sqrtPriceLimitX96: 0
});
validateFeeTier(poolFee);
(amountOut,,,) = quoterV2.quoteExactInputSingle(quoteExactInputSingleParams);
}
However, the intended behavior in _protocolWithdraw
requires calculating input tokens (_backgroundSwap
) needed to obtain assets_
amount of output tokens (_asset
). In this case, quoterV2.quoteExactOutputSingle
instead of quoterV2.quoteExactInputSingle
should be used.
Second, _protocolWithdraw
adds MAX_SLIPPAGE
percentage to the input token amount:
uint256 swapAssets = expectedOutput.mulDiv(100_00 + MAX_SLIPPAGE, 100_00, Math.Rounding.Floor);
This addition is unnecessary. The quote simulates the actual swap, and both operations occur in the same transaction. No slippage occurs between simulation and execution. Instead of withdrawing extra tokens, the function should specify minAmountOut
in _swapExactTokenToToken
:
-uint256 swappedAmount = _swapExactTokenToToken(address(_backgroundSwap), _asset, assetsToWithdraw, 0);
+uint256 swappedAmount = _swapExactTokenToToken(address(_backgroundSwap), _asset, assetsToWithdraw, assets_);
Impact
Each background swap in _protocolWithdraw
withdraws an extra MAX_SLIPPAGE
percentage from _morphoVault
. These excess tokens remain idle in the strategy, reducing yield potential.
Recommendations
First, add a getExpectedInput
function to UniswapV3HelperV1 that uses quoterV2.quoteExactOutputSingle
to calculate required input tokens for a specific output amount. Replace getExpectedOutput
with this function in _protocolWithdraw
.
Second, execute _swapExactTokenToToken
with minAmountOut
set to assets_
, and also add an isValidQuote
check to ensure the swap price is within a valid range.
-uint256 swappedAmount = _swapExactTokenToToken(address(_backgroundSwap), _asset, assetsToWithdraw, 0);
+isValidQuote(address(_backgroundSwap), _asset, assetsToWithdraw, assets_);
+uint256 swappedAmount = _swapExactTokenToToken(address(_backgroundSwap), _asset, assetsToWithdraw, assets_);
Remediation
This issue has been acknowledged by Blueprint Finance, and a fix was implemented in commit 0a705658↗.