skip to content
← Go back

Memware: Generalised Frontrunners

Memware: Generalised Frontrunners

If there was malware native to crypto, it would be generalised frontrunners (GFs). These pesky critters analyse each transaction that enters the mempool, and like sharks, detecting vulnerable pending transactions, striking when they observe a profitable transaction. In this article we'll unravel, step-by-step, how one of these sophiscated creatures work in hopes to shed light on the depths of the mempool.

How do they work?

GFs are fundamentally replication bots that scan the mempool’s pending transactions and dissect each one individually, taking apart the calldata and, for sophiscated bots, the underlying contract(s) that are being interacted with. Once anatomised, the bot replaces any relevant variables with their own, e.g. their own address, copying the functionality of the original transaction.

Generalisation is very powerful when the correct measures are in place, e.g. making sure the contract being interacted with is not a honeypot. They have the ability to steal any type of transaction while it’s pending for confirmation - whether it’s an exploit, arbitrage, liquidation, you name it - and submit it with higher gas than the original to be confirmed before it.

Replicating a transaction

Lets say we’re on Ethereum mainnet and have the following contract that requires owner to be the caller of exploit() in order to commence an exploit on another contract.

contract MyExploit {
    address owner = address(originalAddress);
    function exploit() external {
        require(owner == msg.sender);
        // logic to exploit another contract...
    }
}

originalAddress in this instance creates a transaction to execute exploit() with the following transaction that will then be submitted into the mempool where it will be pending/awaiting confirmation:

Transaction {
    from: originalAddress,
    to: MyExploit,
    input: exploit(),
    gas: 500_000
    gas_price: 10_000_000_000
}

While the transaction is pending, our bot detects, replicates and submits it to a local simulated environment that forks the mainnet, with 1 adjustment:

from: originalAddress -> from: ourAddress

The new modified transaction is:

Transaction {
    from: ourAddress, // this is replaced with our address
    to: MyExploit,
    input: exploit(),
    gas: 500_000
    gas_price: 10_000_000_000
}

Notice how the MyExploit contract requires the caller to be the same as the owner address. This means our transaction would fail considering owner in MyExploit is originalAddress, not ourAddress. However, if the contract didn’t have the owner requirement, this modified transaction would have been able to access the exploit function.

Modifying the contract

Now the bot moves onto the next attempt, re-deploying the contract with modified variable.

In this case, require(owner == msg.sender) is preventing us from copying the transaction so we need to modifiy owner to be ourAddress in replacement of originalAddress.

The bot grabs the bytecode of the contract, detects where the variable is stored (obviously, it’s not a simple as this. You would need to create a bytecode decoder to figure out where variables are stored, what their types are, how many params a function takes, what is happening in the function, etc.) and deploys the following modified contract:

contract MyModifiedExploit {
    address owner = address(ourAddress); // this is replaced with our address
    function exploit() external {
        require(owner == msg.sender);
        // logic to exploit another contract...
    }
}

Our modified transaction needs to update the to variable:

Transaction {
    from: ourAddress,
    to: MyModifiedExploit, // this is replaced with our contract
    input: exploit(),
    gas: 500_000
    gas_price: 10_000_000_000
}

Now we have crafted a transaction that has access to the exploit code 🎉!

But wait, there’s something wrong here…

If we submit this transaction with the exact same gas and gas_price as the original transaction it will be confirmed after the it as they submitted it first.

Why’s that?

Frontrunning the copied transaction

Different blockchains have different confirmation rules (gas, submission time, etc). On Ethereum it is based solely on the total gas paid. Since this transaction is on Ethereum it is perfect for us because they can submit their transaction before ours and we can deploy the modified contract, edit their transaction and increase the gas_price to 10_000_000_001, ordering our transaction before theirs (thanks to 10_000_000_001 being larger than 10_000_000_000)!

Note: there are some more gas variables. However this is overall concept - pay more.

Now the following transaction with be confirmed before the original one we copied, frontrunning it and reaping the profit for ourselves. That poor security researcher spent all that time developing that exploit only for it to be blindly stolen from him (lmfao).

Final

GFs are incredibly fascinating programs that can enable the average joe programmer to replicate the hard work and expertise of entire teams, exotic/unknown strategies being captured by a solo searchers/smol groups, the transaction outputs of highly optimised arbitrage and liquidation systems, even whitehat AND blackhat exploits. GFs are theoretically the ultimate strategy once perfected. No transaction is safe with these lurkers around.

Having all that said, they do require a lot of work to build. If it was easy to build a system such as this everyone would do it ;)

Nonetheless, don’t let me discourage you anon. There’s a bundle of niche skills you will aquire while journeying down the path of building a GF - from learning to reverse engineer bytecode, to emulating virtual machines, these skills are known by few and will ensure a bright future, even outside of crypto!

I appreciate you for taking the time to read this article. I hope you found value in this, anon!

Recent Articles