The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: medium
Valid

Swap function will have unpredictable results for the user, possibly leaving the loan at the edge of collateralization or with no tokens received

Summary

SmartVaultV3 has a swap function that allows the loaner to swap one the collateral for another accepted token, without having to withdraw. As parameters the function takes, the token in, the token out, and the amount to swap. Out of the user control is the minimum amount to receive, and the fee of the pool to use. The fee used is always 3000 (0,3%). The minimum amount out is calculated (calculateMinimumAmountOut function) and it can range from 0 to the minimum amount to keep the vault collateralized.

The user can make a swap with unintended losses being incurred, be it from an actor frontrunning or from the liquidity pool not having enough liquidity. Control over the minimum amount out and the fee tier should be given to the user.

Vulnerability Details

The swap function starts by calculateMinimumAmountOut(), which checks the minimum amount of tokenOut to receive to keep the loan at the minimum of collateralization. It will then call UniswapV3 Router with the calculated minimum amount of token out and the hardcoded fee of 3000 (0,3%).

Impact

Some awful scenarios can occur:

  1. User gets nothing for his swap

  • The user will make a swap of tokenA to tokenB.

  • The amount defined by the user is 10. Looking at the current market rate between TokenA and TokenB, he expects to receive 5 TokenB

  • 10 TokenA or 5 TokenB is an amount unnecessary for the health of the vault. The MinimumAmountOut is defined as 0.

  • The swap is done and the user instead of 5 TokenB, receives dust amounts of TokenB.

  1. User loan is put at the edge of liquidation, being liquidated a few blocks after the swap

  • The user will make a swap of tokenA to tokenB.

  • The amount defined by the user is 10. Looking at the current market rate between TokenA and TokenB, he expects to receive 5 TokenB

  • 2 TokenB is the minimum necessary to keep the vault at the edge of liquidation. The MinimumAmountOut is defined as 2 TokenB.

  • The swap is done and the user instead of 5 TokenB, receives 2 TokenB.

  • A few blocks after with a slight variation on the price of the collateral tokens the vault is undercollateralised. The vault is liquidated and the user loses his collateral.

The reason for receiving less amounts of TokenB than predicted by the current market rate, could be that there is a frontrunner that used up liquidity for the swap, or that the used UNIV3 pool simply did not have enough liquidity.

There is not enough predictability on the results of the swap.

One of the accepted tokens, PAXG, at the moment has barely any liquidity on UniswapV3 for any pool fee.

UNIV3 PAXG pools 07.01.23

Tools Used

Manual review.

Recommendations

The control of amount out and fee tier should be given to the user, and a check should be made, if the amount out of the received token will keep the vault collateralized.

    @@ -209,21 +216,26 @@ contract SmartVaultV3 is ISmartVault {
        uint256 collateralValueMinusSwapValue = euroCollateral() - calculator.tokenToEur(getToken(_inTokenSymbol), _amount);
        return collateralValueMinusSwapValue >= requiredCollateralValue ?
            0 : calculator.eurToToken(getToken(_outTokenSymbol), requiredCollateralValue - collateralValueMinusSwapValue);
    }
-    function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount) external onlyOwner {
+    function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount, uint256 _minAmountOut, uint24 _fee) external onlyOwner {
        uint256 swapFee = _amount * ISmartVaultManagerV3(manager).swapFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
        address inToken = getSwapAddressFor(_inToken);
        uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount);
+        require(_minAmountOut >= minimumAmountOut);
        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
                tokenIn: inToken,
                tokenOut: getSwapAddressFor(_outToken),
-                fee: 3000,
+                fee: _fee,
                recipient: address(this),
                deadline: block.timestamp,
                amountIn: _amount - swapFee,
-                amountOutMinimum: minimumAmountOut,
+                amountOutMinimum: _minAmountOut,
                sqrtPriceLimitX96: 0
            });
        inToken == ISmartVaultManagerV3(manager).weth() ?
            executeNativeSwapAndFee(params, swapFee) :
Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Slippage-issue

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

hardcoded-fee

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.