Target Addresses

Contract

//SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.19;
import "openzeppelin-contracts/contracts/access/Ownable.sol";

import "contracts/MevShareCaptureLogger.sol";
import "./MevShareCTF.sol";

contract MevShareCTFSimple is MevShareCTFBase {
    uint256 public activeBlock;

    uint256 immutable captureId;

    event Activate();

    constructor(MevShareCaptureLogger _mevShareCaptureLogger, uint256 _captureId) MevShareCTFBase(_mevShareCaptureLogger) payable {
        captureId = _captureId;
    }

    function activateRewardSimple() external payable onlyOwner {
        activeBlock = block.number;
        emit Activate();
    }

    function claimReward() external {
        require (activeBlock == block.number);
        activeBlock = 0;
        mevShareCaptureLogger.registerCapture(captureId, tx.origin);
    }
}

Capture Pending Txs

import MevShareClient, {
    BundleParams,
    HintPreferences,
    IPendingBundle,
    IPendingTransaction,
    TransactionOptions
} from "@flashbots/mev-share-client"
import { Contract, JsonRpcProvider, Wallet } from 'ethers'
import { MevShareCTFSimple_ABI } from './abi'

import dotenv from "dotenv"
dotenv.config()

const TX_GAS_LIMIT = 400000
const MAX_GAS_PRICE = 20n
const MAX_PRIORITY_FEE = 20n
const GWEI = 10n ** 9n

const RPC_URL = process.env.RPC_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY || Wallet.createRandom().privateKey

const provider = new JsonRpcProvider(RPC_URL)
const signer = new Wallet(PRIVATE_KEY, provider)
const mevShare  = MevShareClient.useEthereumGoerli(signer)

async function main() {

    mevShare.on('transaction', async ( pendingTx: IPendingTransaction ) => {
        console.log(pendingTx)
    })

    // mevShare.on('bundle', async ( pendingTx: IPendingTransaction ) => {
    //     // callback to handle pending bundle
    //     console.log(pendingTx)
    // })
}

main()

Event Stream

Untitled

Untitled

Untitled

104 is different:

Untitled

Untitled

So the pattern here is logs: undefined + to: null + gasUsed: 30000n.

Solution

101 102 103 104 are very similar, the difference is the struct of PendingTransaction object.

import MevShareClient, {
    BundleParams,
    HintPreferences,
    IPendingBundle,
    IPendingTransaction,
    TransactionOptions
} from "@flashbots/mev-share-client"
import { Contract, JsonRpcProvider, Wallet } from 'ethers'
import { MevShareCTFSimple_ABI } from './abi'

import dotenv from "dotenv"
dotenv.config()

const TX_GAS_LIMIT = 400000
const MAX_GAS_PRICE = 20n
const MAX_PRIORITY_FEE = 20n
const GWEI = 10n ** 9n

const RPC_URL = process.env.RPC_URL
const PRIVATE_KEY = process.env.PRIVATE_KEY || Wallet.createRandom().privateKey

const provider = new JsonRpcProvider(RPC_URL)
const signer = new Wallet(PRIVATE_KEY, provider)
const mevShare  = MevShareClient.useEthereumGoerli(signer)

const MevShareCTFSimple_ADDRESS = "0x20a1a5857fdff817aa1bd8097027a841d4969aa5"
const mevShareCTFSimple = new Contract(MevShareCTFSimple_ADDRESS, MevShareCTFSimple_ABI, signer)

function transactionIsRelevant(pendingTx: IPendingTransaction, PAIR_ADDRESS: string) {
    return pendingTx.to === MevShareCTFSimple_ADDRESS.toLowerCase() ||
        ((pendingTx.logs || []).some(log => log.address === MevShareCTFSimple_ADDRESS.toLowerCase())) ||
        (pendingTx.logs === undefined && pendingTx.to === null && pendingTx.gasUsed === 30000n)
}

async function getSignedBackrunTx(nonce: number ) {
    const backrunTx = await mevShareCTFSimple.claimReward.populateTransaction()
    const backrunTxFull = {
        ...backrunTx,
        chainId: 5,
        maxFeePerGas: MAX_GAS_PRICE * GWEI,
        maxPriorityFeePerGas: MAX_PRIORITY_FEE * GWEI,
        gasLimit: TX_GAS_LIMIT,
        nonce: nonce
    }
    return signer.signTransaction(backrunTxFull)
}

async function backrunAttempt( currentBlockNumber: number, nonce: number, pendingTxHash: string ) {
    const backrunSignedTx = await getSignedBackrunTx(nonce)
    try {
        const sendBundleResult = await mevShare.sendBundle({
            inclusion: { block: currentBlockNumber + 1 },
            body: [
                { hash: pendingTxHash },
                { tx: backrunSignedTx, canRevert: false }
            ]
        },)
        console.log('Bundle Hash: ' + sendBundleResult.bundleHash)
    } catch (e) {
        console.log('Error: ', e)
    }
}

async function main() {

    mevShare.on('transaction', async ( pendingTx: IPendingTransaction ) => {
        // console.log(pendingTx)

        if (transactionIsRelevant(pendingTx, MevShareCTFSimple_ADDRESS)) {
            console.log(pendingTx)
            const currentBlockNumber = await provider.getBlockNumber()
            const nonce = await signer.getNonce('latest')
            backrunAttempt(currentBlockNumber, nonce, pendingTx.hash);
        }
    })
}

main()