Published on
【Free MEV系列】| Subway 夹击策略
Authors
  • avatar
    Name
    thinkingchaindotapp
    Twitter

subway

第三方仓库地址

合约

整个文件夹下面,核心就是一个MEV bot的合约:

// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0;

import "./interface/IERC20.sol";
import "./lib/SafeTransfer.sol";

contract Sandwich {
    using SafeTransfer for IERC20;

    address internal immutable owner;

    // transfer(address,uint256)
    bytes4 internal constant ERC20_TRANSFER_ID = 0xa9059cbb;

    // swap(uint256,uint256,address,bytes)
    bytes4 internal constant PAIR_SWAP_ID = 0x022c0d9f;

    receive() external payable {}

    constructor(address _owner) {
        owner = _owner;
    }

    // *** Receive profits from contract *** //
    function recoverERC20(address token) external {
        require(msg.sender == owner, "not owner");
        IERC20(token).safeTransfer(
            msg.sender,
            IERC20(token).balanceOf(address(this))
        );
    }

    /*
        Fallback function where you do your frontslice and backslice

        NO UNCLE BLOCK PROTECTION IN PLACE, USE AT YOUR OWN RISK

        Payload structure (abi encodePacked)

        - token: address        - Address of the token you're swapping
        - pair: address         - Univ2 pair you're sandwiching on
        - amountIn: uint128     - Amount you're giving via swap
        - amountOut: uint128    - Amount you're receiving via swap
        - tokenOutNo: uint8     - Is the token you're giving token0 or token1? (On uniswap V2 pair)

        Note: This fallback function generates some dangling bits
    */
    fallback() external payable {
        // Assembly cannot read immutable variables
        address memOwner = owner;

        assembly {
            // only owner
            if iszero(eq(caller(), memOwner)) {
                // Ohm (3, 3) makes your code more efficient
                revert(3, 3)
            }

            // bytes20
            let token := shr(96, calldataload(0x00))
            // bytes20
            let pair := shr(96, calldataload(0x14))
            // uint128
            let amountIn := shr(128, calldataload(0x28))
            // uint128
            let amountOut := shr(128, calldataload(0x38))
            // uint8
            let tokenOutNo := shr(248, calldataload(0x48))

            // **** calls token.transfer(pair, amountIn) ****

            // transfer function signature
            mstore(0x7c, ERC20_TRANSFER_ID)
            // destination
            mstore(0x80, pair)
            // amount
            mstore(0xa0, amountIn)

            let s1 := call(sub(gas(), 5000), token, 0, 0x7c, 0x44, 0, 0)
            if iszero(s1) {
                revert(3, 3)
            }

            // ************
            /* 
                calls pair.swap(
                    tokenOutNo == 0 ? amountOut : 0,
                    tokenOutNo == 1 ? amountOut : 0,
                    address(this),
                    new bytes(0)
                )
            */

            // swap function signature
            mstore(0x7c, PAIR_SWAP_ID)
            // tokenOutNo == 0 ? ....
            switch tokenOutNo
            case 0 {
                mstore(0x80, amountOut)
                mstore(0xa0, 0)
            }
            case 1 {
                mstore(0x80, 0)
                mstore(0xa0, amountOut)
            }
            // address(this)
            mstore(0xc0, address())
            // empty bytes
            mstore(0xe0, 0x80)

            let s2 := call(sub(gas(), 5000), pair, 0, 0x7c, 0xa4, 0, 0)
            if iszero(s2) {
                revert(3, 3)
            }
        }
    }
}

使用内联汇编在fallback中书写了功能:将代币转到池子(token0/token1,双向),然后swap。

Bot代码

算法

这个仓库的核心是做三明治攻击,场景是:

  1. Attacker先执行 WETH -> TOKEN 交易,推高 token 的价格。
  2. Victim执行 WETH -> TOKEN 交易,进一步推高 token 的价格。
  3. Attacker执行 TOKEN -> WETH 交易,卖出 token 获利。

为了盈利,设计了一个二分查找的算法,如下图所示(你可以在此链接中找到原出处):

二分查找

算法的逻辑:

  • Victim有一个最小的输出金额a,我们在操纵价格之后,不得使得他的输出小于a
  • 假设Victim的实际输出为金额bb必须大于等于a,并且b越贴近于a,我们的获利越大
  • Attacker设置一个缓冲区tolerance,代表我们期待b需要满足的范围。tolerance的范围设置为:[a, a * 1.01],我们允许1%的缓冲范围
  • 算法需要不断调整Attacker's ETH amontIn,使得b位于tolerance缓冲区中
  • b位于tolerance时,也要满足profit大于0,才算完成搜索

分析一下图中的两种情况(其实还有其他情况,就不多举例了):

  • scenario 1
    • Step 1:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance的右边,说明我们还可以抬高Token价格增加获利空间,可以在此基础上继续抬高价格
    • Step 2:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance的右边,说明我们还可以抬高Token价格增加获利空间,可以在此基础上继续抬高价格
    • Step 3:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance的左边,说明我们使Token价格抬高得太多了导致Victim不愿意了,应该在此基础上拉低价格
    • Step 4:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance中:如果profit大于0,则推出,否则继续迭代直到profit大于0
  • scenario 2
    • Step 1:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance的左边,说明我们使Token价格抬高得太多了导致Victim不愿意了,应该在此基础上拉低价格
    • Step b:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance的左边,说明我们使Token价格抬高得太多了导致Victim不愿意了,应该在此基础上拉低价格
    • Step c:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance的右边,说明我们还可以抬高Token价格增加获利空间,可以在此基础上继续抬高价格
    • Step d:attacker输入一定数量的ETH之后,Victim的最终输出金额b位于tolerance中:如果profit大于0,则推出,否则继续迭代直到profit大于0

分析代码:

  • 这是核心的二分查找代码
export const binarySearch = (
  left, // Lower bound
  right, // Upper bound
  calculateF, // Generic calculate function
  passConditionF, // Condition checker
  tolerance = parseUnits("0.01") // Tolerable delta (in %, in 18 dec, i.e. parseUnits('0.01') means left and right delta can be 1%)
) => {
  • 向外暴露的、我们所使用的函数,包装了binarySearch()
export const calcSandwichOptimalIn = (
  userAmountIn,
  userMinRecvToken,
  reserveWeth,
  reserveToken
) => {
  • 计算、查看三明治攻击的state
export const calcSandwichState = (
  optimalSandwichWethIn,
  userWethIn,
  userMinRecv,
  reserveWeth,
  reserveToken
) => {

flashbot

  • 如果存在错误则捕获,没有则返回原来的结果
export const sanityCheckSimulationResponse = (sim) => {
  • 模拟执行
export const callBundleFlashbots = async (signedTxs, targetBlockNumber) => {
  • 将交易对象序列化为原始交易数据(十六进制字符串)
export const getRawTransaction = (tx) => {
  • 发送Bundles交易到flashbot
export const sendBundleFlashbots = async (signedTxs, targetBlockNumber) => {

main

流程如下:

1.不断监听memory pool中的status为pending的交易,然后开始处理它

2.从pending的交易中解析出详情:(1)交易存在(我们只监听ETH=>Token的交易);(2)交易是和UniswapV2Router交互;(3)解码calldata;(4)交易还没到deadline

3.根据victim的交易详情,计算出一些准备数据:(1)我们用WETH换成token,token的最少数量;(2)Pair地址;(3)Reserve数量;

4.算法开始,找到profit

5.根据profit的结果,计算出我们三明治交易的输入内容,然后准备

6.准备区块状态信息

7.打包三明治交易:(1)我们的第一笔交易,套高价格;(2)Victim的交易;(3)我们的第二笔交易,换回,拿到利润

8.flashbot模拟交易,看看是否可行

9.计算gas消耗,还有贿赂费,看看是否盈利

10.一切就绪,发射🚀三明治交易

Tags