TM#014 — How MEV bots work
💡 All code for this post is inside this Github repository
Overview
Miner Extractable Value (MEV) is a term used to describe the potential profit that miners can extract from the reordering or manipulation of transactions within a block before it is added to the blockchain. MEV arises due to the inherent order dependence of transactions in many blockchain networks, particularly those utilizing Proof of Work (PoW) consensus mechanisms. MEV can be both beneficial and harmful, as it provides opportunities for profit but also raises concerns about fairness, transparency, and security within the ecosystem.
Practice MEV Bot (Frontrunner)
The bot we are going to code today is what we call a “Frontrunner” or a “Searcher”. As the name suggests, it frontruns your transactions, meaning it simulates your transaction to see if anyone can fill it and end up in a profit by just copying and getting their transaction to the miners before yours.
MEV bots are scanning the mempools all the time and there is no known limit to which extent they might be looking into your transactions. From just simple swap sandwich to deploying multiple contracts, taking flash loans, paying 1000 of dollars in Gas fees as Bribe to Miners to get their transaction mined before you.
Smart Contract
Solidity Contract: I have kept it very simple just for the purpose of this demonstration. Please don’t deploy it to the mainnet as its for educational purposes only.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract FrontRunMe {
mapping (address => bytes32) public addressToSecret;
// * Deposit and set a Secret Passcode
function deposit(bytes32 _secret) public payable {
addressToSecret[msg.sender] = _secret;
}
// @params
// _user : Address of the User who has the key
// _secret : Secret Passcode of the User
// @desc : Leaving these vulnerabilities intentionally to make frontrunable, while
// staying within the scope of the activity
function withdraw(address _user, bytes32 _secret) public {
// * Check if Caller has the User's Secret Passcode
require(addressToSecret[_user] == _secret, "Wrong Password!");
// * Send all Contract Balance to Caller
(bool sent, ) = msg.sender.call{value: address(this).balance}("");
require(sent, "Failed to send Ether");
}
}
Let me explain the contract and then we will code the MEV bot.
Although it's pretty self-explanatory but I will give you an abstract overview of what’s happening.
Any user can deposit funds and a passcode for later withdrawing it. But we are allowing anyone to submit a user’s address and his key to get the withdraw (if you know the pass you must be authorized right?).
But what if someone can see you passing an address
and secret
and do the withdraw
transaction before you? He would get all the money. But that’s not possible, right? right?
Always remember Ethereum is a Dark Forest (Paradigm Dark Forest Post). So, the wandering beasts i.e., MEV bots and searchers are always watching the Ethereum mempool (where all transactions are broadcasted before miners pick them up and create blocks — thus finalizing them) among them will simulate (devour) your transaction and see if they get any profit out of it.
And submit it with a higher gas fee using a Dark RPC (transactions not broadcasted to the mempool, instead directly sent to miners). Flashbots is one of the most used service in this regard.
Now let’s see how one might be running such a bot to look at your transactions:
GPS (Generalized Profit Seeker) Bot Code
The bot is doing these steps in the code:
- Initializing
Providers
andWallets
// 1. Setup ethers, required variables, contracts and start function
const { Wallet, ethers } = require("ethers");
const {
FlashbotsBundleProvider,
FlashbotsBundleResolution,
} = require("@flashbots/ethers-provider-bundle");
// 1. Setup Providers and Wallets
// Setup user modifiable variables
const flashbotsUrl = "<https://relay-goerli.flashbots.net>";
const httpProviderUrl =
"http-endpoint";
const wsProviderUrl =
"wss-endpoint";
// Copy Pvt key from Metamask and paste here
const privateKey =
"";
//* Setup Contract to Watch
// ? Normally the bot will scan every Transaction & Contract but due to limited resources we know our target
const targetContractAddress = "0x94a4BFb8D582279Fd79feF4686b8062a6a1b5cee";
// Chain Goerli
const chainId = 5;
// Initialize Providers
const provider = new ethers.providers.JsonRpcProvider(httpProviderUrl);
const wsProvider = new ethers.providers.WebSocketProvider(wsProviderUrl);
// Setup Signer Wallet for Bot
const signingWallet = new ethers.Wallet(privateKey, provider);
2. Listening to all “Pending” Transactions in the mempool and calling processTransaction
for each transaction
// 3. Start Listening for pending transactions
const start = async () => {
console.log("Listening on transaction for the chain id", chainId);
console.log("Creating FLashboat Provider")
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
signingWallet,
flashbotsUrl
);
// Listen to all transactions in the mempool
// ? With a status = Pending
wsProvider.on("pending", (tx) => {
processTransaction(tx, flashbotsProvider);
});
};
start();
2. processTransaction
: this is the main function that does all the heavy lifting.
a) It takes the transaction hash from wsProvider
and getTransaction
to get all the data of that particular transaction.
// 2. Process the Transaction to see if we should frontrun it
const processTransaction = async (txHash, flashbotsProvider) => {
let tx = null
// * 3. Check if someone called our targeted Smart Contract
try {
tx = await provider.getTransaction(txHash);
// console.log(transaction)
if (tx.to.toLowerCase() == targetContractAddress.toLowerCase()) {
console.log("Someone intetracted with our target contract");
console.log(tx);
}
else {
return false
}
} catch (err) {
return false;
}
b) Checks if the Transaction is with our Targeted Contract (General Seekers usually scan every transaction for profit, but its implementation would be very complex and out of the scope of this post)
// Construct the transaction parameters for copying
const copiedTransaction = {
to: tx.to,
value: tx.value,
data: tx.data,
gasPrice: totalGasFee,
gasLimit: 300000, // Set the gas price including bribe
nonce: await provider.getTransactionCount(signingWallet.address, "latest"), // Get the next nonce
chainId: tx.chainId,
};
c) Copies the Transaction of the sender and use flashboats to simulate it for checking if the Transaction will work or fail on the mainnet
// Simulate and send transactions
console.log("Simulating...");
const simulation = await flashbotsProvider.simulate(
signedTransactions,
blockNumber + 1
);
if (simulation.firstRevert) {
return console.log("Simulation error", simulation.firstRevert);
} else {
console.log("Simulation success", simulation);
}
d) Finally, we send it directly to miners with Higher Gas Fee
// Send transactions with flashbots
let bundleSubmission;
flashbotsProvider
.sendRawBundle(signedTransactions, blockNumber + 1)
.then((_bundleSubmission) => {
bundleSubmission = _bundleSubmission;
console.log("Bundle submitted", bundleSubmission.bundleHash);
return bundleSubmission.wait();
})
.then(async (waitResponse) => {
console.log("Wait response", FlashbotsBundleResolution[waitResponse]);
if (waitResponse == FlashbotsBundleResolution.BundleIncluded) {
console.log("-------------------------------------------");
console.log("-------------------------------------------");
console.log("----------- Bundle Included ---------------");
console.log("-------------------------------------------");
console.log("-------------------------------------------");
} else if (
waitResponse == FlashbotsBundleResolution.AccountNonceTooHigh
) {
console.log("The transaction has been confirmed already");
} else {
console.log("Bundle hash", bundleSubmission.bundleHash);
try {
console.log({
bundleStats: await flashbotsProvider.getBundleStats(
bundleSubmission.bundleHash,
blockNumber + 1
),
userStats: await flashbotsProvider.getUserStats(),
});
} catch (e) {
return false;
}
}
});
};
It will inform us if our Transaction Bundle (usually there are more than one transaction to make anything meaningful but for our purpose one was enough) was included in a block.
It’s not a full-fledge MEV bot with dedicated Full Node so it will be difficult to run it with Alchemy, Infura etc., and frontrun anybody but it is possible. You might fail a lot of the times though.
Practical Example
I have tried it myself and here is how my bot Copied and Submitted the transaction before the original sender:
Original Sender (0xE16899E3Ec71860b7631A6f3f8d072c32aF0d7a4)
MEV Bot (0xE16899E3Ec71860b7631A6f3f8d072c32aF0d7a4)
You can see that the MEV bot’s transaction was executed before the Original Sender thanks to its efforts watching the mempool and copying profitable transactions.
Complete List of Transactions on Goerli.
MEV Bot’s Transaction
Original Sender’s Transaction
Apparently, both of them look the same. But notice the difference in gas price. The Bot paid 10x the amount of Gas to execute its transaction before the Original Sender.
Conclusion
This is how MEV works in essence, you can look more into flashbots (leveling the playing field by providing everyone the opportunity for MEV — previously only miners could do it) and google more complicated MEV bots (Frontrunners, Sandwichers, Searchers etc) to see how they work.
Thanks for reading and remember Ethereum’s mempool is a very dangerous place. So, be careful before you withdraw money from a vulnerable contract because
🔴 Bots are always watching
This article is written by Muhammad Umar, a Blockchain Developer at Antematter.io.