以太坊抢跑攻击与防御

  • DeCert.me
  • 发布于 2025-12-16 14:48
  • 阅读 25

抢跑(Front-Running)是区块链中独特的攻击向量,源于区块链的透明性和交易排序机制。在传统金融市场中这是非法的,但在区块链上由于技术特性,变得难以完全防范。理解抢跑攻击对于构建安全的 DeFi 应用至关重要。

什么是抢跑

基本概念

抢跑(Front-Running):攻击者监控内存池(mempool)中的待处理交易,在目标交易被打包前插入自己的交易,从而获利。

类比: 想象你在拍卖会上出价 100 元买一幅画,但在拍卖师确认前,有人听到了你的出价,抢先出价 101 元。这就是抢跑。

区块链环境中的抢跑

  1. 用户A发送交易:买入 1000 个代币,gas 价格 50 gwei
  2. 攻击者监测到:在 mempool 中看到这笔交易
  3. 攻击者抢先:发送相同交易,但 gas 价格 100 gwei
  4. 矿工优先打包:攻击者的交易先被执行
  5. 用户A交易执行:以更差的价格成交或失败

抢跑的类型

1. 经典抢跑(Displacement)

攻击者完全替代受害者的交易。

场景:抢注 ENS 域名

contract ENSRegistrar {
    mapping(bytes32 => address) public owners;

    function register(string memory name) public payable {
        bytes32 nameHash = keccak256(abi.encodePacked(name));

        // ❌ 漏洞:先到先得,容易被抢跑
        require(owners[nameHash] == address(0), "Already registered");
        require(msg.value >= registrationFee, "Insufficient fee");

        owners[nameHash] = msg.sender;
    }
}

攻击过程

  1. Alice 发送交易注册 "vitalik.eth",gas = 50
  2. Bob 的机器人检测到交易
  3. Bob 发送相同交易,gas = 100
  4. Bob 的交易先执行,获得域名
  5. Alice 的交易失败

2. 三明治攻击

攻击者在受害者交易前后插入交易获利。

场景DEX 套利

// 简化的 DEX
contract SimpleDE X {
    uint public price = 100;  // 1 ETH = 100 tokens

    function buyTokens() public payable {
        // ❌ 大额购买会影响价格
        uint amount = msg.value * price;
        price = price * 110 / 100;  // 价格上涨 10%

        tokens[msg.sender] += amount;
    }

    function sellTokens(uint amount) public {
        price = price * 90 / 100;  // 价格下跌 10%
        uint ethAmount = amount / price;

        payable(msg.sender).transfer(ethAmount);
    }
}

攻击

1. Alice 的交易:买入 10 ETH 的代币(在 mempool 中)
2. 攻击者前置交易:买入 5 ETH 的代币(gas 更高)
   - 价格从 100 涨到 110
3. Alice 的交易执行:以更高价格(110)买入
   - 价格从 110 涨到 121
4. 攻击者后置交易:卖出代币(gas 更高)
   - 以 121 的价格卖出,获利

3. 抑制攻击(Suppression)

攻击者通过高 gas 费阻止某些交易执行。

场景:清算竞争

contract LendingProtocol {
    function liquidate(address borrower) public {
        // 清算获得 5% 奖励
        uint reward = debt[borrower] * 5 / 100;

        // ❌ 可能被抑制
        require(isUnderCollateralized(borrower), "Not liquidatable");

        payable(msg.sender).transfer(reward);
    }
}

攻击

  • 攻击者用极高 gas 发送清算交易
  • 其他清算者的交易被挤出区块
  • 攻击者独占清算奖励

MEV(矿工可提取价值)

什么是 MEV

MEV(Maximal Extractable Value):矿工/验证者通过控制交易顺序可以提取的最大价值。

MEV 的表现形式

  1. 交易排序:矿工可以任意排列区块内的交易
  2. 交易插入:矿工可以插入自己的交易
  3. 交易审查:矿工可以排除某些交易

MEV 实例

Uniswap 套利机器人

区块 N:
1. 矿工看到一笔大额 USDC->ETH 交易
2. 矿工在前面插入:ETH->USDC(价格低)
3. 受害者交易执行:USDC->ETH(推高 ETH 价格)
4. 矿工在后面插入:USDC->ETH(价格高,获利)

2020-2021 年,MEV 为矿工带来了数亿美元的额外收入。

防御策略

策略 1:Commit-Reveal 模式

将操作分为两步:提交哈希,稍后揭示。

pragma solidity ^0.8.0;

contract CommitReveal {
    struct Commitment {
        bytes32 commit;
        uint revealBlock;
        bool revealed;
    }

    mapping(address => Commitment) public commitments;

    // 第 1 步:提交哈希
    function commit(bytes32 _commit) public {
        require(commitments[msg.sender].revealBlock == 0, "Already committed");

        commitments[msg.sender] = Commitment({
            commit: _commit,
            revealBlock: block.number + 10,  // 10 个区块后才能揭示
            revealed: false
        });
    }

    // 第 2 步:揭示原始值
    function reveal(string memory value, bytes32 salt) public {
        Commitment storage c = commitments[msg.sender];

        require(c.revealBlock != 0, "Not committed");
        require(block.number >= c.revealBlock, "Too early");
        require(!c.revealed, "Already revealed");

        // 验证哈希
        bytes32 hash = keccak256(abi.encodePacked(value, salt, msg.sender));
        require(hash == c.commit, "Invalid reveal");

        c.revealed = true;

        // 执行业务逻辑
        processValue(value);
    }

    function processValue(string memory value) internal {
        // 实际的业务逻辑
    }
}

优点

  • ✅ 攻击者无法提前知道操作内容
  • ✅ 适用于拍卖、投票等场景

缺点

  • ⚠️ 需要两次交易,用户体验较差
  • ⚠️ 增加 gas 成本

策略 2:滑点保护

设置可接受的最大/最小价格。

pragma solidity ^0.8.0;

contract DEXWithSlippage {
    function swap(
        address tokenIn,
        address tokenOut,
        uint amountIn,
        uint minAmountOut  // ✅ 滑点保护
    ) public {
        uint amountOut = calculateSwap(tokenIn, tokenOut, amountIn);

        // 检查滑点
        require(amountOut >= minAmountOut, "Slippage too high");

        // 执行交换
        executeSwap(tokenIn, tokenOut, amountIn, amountOut);
    }

    function calculateSwap(
        address tokenIn,
        address tokenOut,
        uint amountIn
    ) internal view returns (uint) {
        // 计算输出数量
    }

    function executeSwap(
        address tokenIn,
        address tokenOut,
        uint amountIn,
        uint amountOut
    ) internal {
        // 执行实际的交换
    }
}

前端实现

// 计算滑点
const expectedOutput = await dex.calculateSwap(tokenIn, tokenOut, amountIn);
const slippageTolerance = 0.5;  // 0.5%
const minOutput = expectedOutput * (1 - slippageTolerance / 100);

// 发送交易
await dex.swap(tokenIn, tokenOut, amountIn, minOutput);

策略 3:使用 Flashbots/私有内存池

Flashbots 允许用户直接向验证者发送交易,绕过公共 mempool。

优点

  • ✅ 交易不会在公共 mempool 中暴露
  • ✅ 防止抢跑
  • ✅ 失败的交易不消耗 gas

使用方式

// 使用 Flashbots RPC
const flashbotsProvider = await FlashbotsBundleProvider.create(
    provider,
    signer
);

// 构建交易束(bundle)
const bundle = [
    {
        signedTransaction: signedTx
    }
];

// 发送到 Flashbots
const bundleReceipt = await flashbotsProvider.sendBundle(bundle, targetBlock);

策略 4:批量处理和时间锁

contract BatchProcessor {
    struct Order {
        address user;
        uint amount;
        uint timestamp;
    }

    Order[] public pendingOrders;
    uint public constant BATCH_INTERVAL = 1 hours;
    uint public lastBatchTime;

    // 用户提交订单
    function submitOrder(uint amount) public {
        pendingOrders.push(Order({
            user: msg.sender,
            amount: amount,
            timestamp: block.timestamp
        }));
    }

    // 批量处理订单(任何人都可以调用)
    function processBatch() public {
        require(
            block.timestamp >= lastBatchTime + BATCH_INTERVAL,
            "Too early"
        );

        // 获取当前价格
        uint price = getCurrentPrice();

        // 处理所有待处理订单
        for (uint i = 0; i < pendingOrders.length; i++) {
            Order memory order = pendingOrders[i];
            executeOrder(order.user, order.amount, price);
        }

        // 清空队列
        delete pendingOrders;
        lastBatchTime = block.timestamp;
    }

    function executeOrder(address user, uint amount, uint price) internal {
        // 执行订单
    }

    function getCurrentPrice() internal view returns (uint) {
        // 获取价格
    }
}

优点

  • ✅ 所有订单以相同价格执行
  • ✅ 消除抢跑优势

缺点

  • ⚠️ 延迟执行
  • ⚠️ 不适合需要即时执行的场景

策略 5:最小成交量限制

contract AntiManipulation {
    uint public constant MIN_TRADE_SIZE = 1 ether;

    function trade(uint amount) public {
        // ✅ 限制最小交易规模
        require(amount >= MIN_TRADE_SIZE, "Trade too small");

        // 交易逻辑...
    }
}

原理:增加攻击成本,使小额抢跑不经济。

最佳实践

合约层面

  1. 实现滑点保护

    require(amountOut >= minAmountOut, "Slippage too high");
  2. 使用 Commit-Reveal(适用场景)

    // 分两步执行敏感操作
  3. 限制价格影响

    uint priceImpact = calculateImpact(amount);
    require(priceImpact <= MAX_IMPACT, "Impact too high");
  4. 时间加权平均价格(TWAP)

    uint price = getTWAP(30 minutes);  // 使用 30 分钟 TWAP

用户层面

  1. 设置合理的滑点

    • 不要设置过大的滑点容忍度
    • 监控实际成交价格
  2. 使用限价单而非市价单

    • 指定明确的最低/最高价格
  3. 选择合适的 gas 价格

    • 不要盲目使用"快速"选项
    • 考虑使用 Flashbots
  4. 分批执行大额交易

    • 减少单笔交易的价格影响

小结

抢跑是区块链独特的挑战

🔍 理解威胁

  • 公开的 mempool 让攻击者有机可乘
  • MEV 是不可避免的现象
  • 影响用户体验和协议公平性

🛡️ 多层防御

  • 合约:滑点保护、批量处理
  • 用户:合理设置、使用私有池
  • 协议:公平排序、透明机制

记住:完全防止抢跑很难,但可以通过多种手段降低风险和影响。

相关资源

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论