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!
Share this Article