It reminded me of the excitement of cracking the Fibonacci series in The Da Vinci Code when I was reading Ethereum is a Dark Forest and Escaping the Dark Forest. I should have plainly forwarded it to my Web 3 frens. But in Web 3, we have a saying.
Don’t trust, verify.
So I decided to walk into the dark forest myself and see what would happen.
To verify the dark forest theory in Ethereum, I made a simple vault-with-a-password situation. I deployed a smart contract with a secret hash and 0.005 Ether (~$10) in it. As long as you feed in the correct code such that the hash value of it is equal to the one in the contract, you can take the Ether. Given the irreversibility of keccak256, others shouldn’t be able to find the secret code and therefore retrieve the Ether.
contract FrontRunMe {
bytes32 private secret;
constructor(bytes32 _secret) payable {
secret = _secret;
}
function send(string calldata code, address to) external {
require(keccak256(bytes(code)) == secret, "Wrong code");
require(address(this).balance > 0, "Zero balance");
(bool success, ) = payable(to).call{value: address(this).balance}("");
require(success, "Payment has failed");
}
}
If the code is correct, the contract will send 0.005 Ether to the to address.
I got sniped, as expected.

When I called the send method with the correct code, my transaction kept pending and ran into a “Zero balance” error; while the other transaction front-runned me and took my 0.005 Ether.
To understand why it could happen, let’s take a look at the regular journey of a transaction.

The user broadcasts the transaction to a public Mempool, i.e. a pool of pending transactions.
Miners greedily pick up the transactions with higher gas prices from the mempool.
Miners verify the selected transactions and compose the next mined block with succeeded ones.
So the front-runner could attack me in this way.

Looping all pending transactions in the mempool.
Execute each transaction and see if the caller or any address in the parameter is getting a net income of Ether.
Replace that address with their own address and see if they could receive a net income as well.
If the answer is true in #3, then submit the transaction with a higher gas price. This could ensure this transaction would be picked up by the miner before the original transaction.
The Dark Forest does exist.
There should be a way to avoid it. Otherwise, why the front-runner wasn’t front-runned by others?
If you stare into the abyss, the abyss stares back at you.
Instead of exposing the transaction to those front-runners, I tried to wrap it into an internal Tx. I thought the internal transaction should be encoded and bots could not find a way to insert their recipient’s address.
The first contract:
contract FrontRunMe {
bytes32 private secret;
constructor(bytes32 _secret) payable {
secret = _secret;
}
function send(string calldata code, address to) external {
require(keccak256(bytes(code)) == secret, "Wrong code");
require(address(this).balance > 0, "Zero balance");
(bool success, ) = payable(to).call{value: address(this).balance}("");
require(success, "Payment has failed");
}
// I forgot to add this method in the first attempt,
// so I had to spend another $20 to deploy a duplicate contract.
receive() external payable {}
}
The second contract:
contract FrontRunMe {
function send(string calldata code, address to) external {}
}
contract Invoker {
FrontRunMe frm;
address private owner;
constructor(address payable _frm) payable {
owner = msg.sender;
frm = FrontRunMe(_frm);
if (msg.value > 0) {
(bool success, ) = payable(_frm).call{value: address(this).balance}("");
require(success, "Payment has failed");
}
}
function invoke(string calldata code, address to) external {
require(msg.sender == owner, "Not owner");
frm.send(code, to); // Call the FrontRunMe contract in an internal transaction
}
}
This time I lower the bait value to 0.003 Ether. It turns out that I was front-runned, again. Courtesy of the owner check in the Invoker contract, the front-runners could not call the invoke method. But they could extract the internal transaction of Invoker contract calling FrontRunMe contract and front-run it.
An internal transaction is as naked as any other transaction in the mempool.
If you can't beat them, join them.
Notice that both front-running transactions have some interaction with some MEV bot or Flashbot, so I study it for a while. If you are interested, I highly recommend their public documentation.
MEV stands for Maximal Extractable Value, you can consider it as an arbitrage opportunity on the blockchain. This could be the tiny free money in my smart contract or some big juicy $9.6 million from a bug. These arbitrage opportunities are accessible to everyone. If A tries to arbitrage, B would catch A and front-run A, then C would try to front-run B. Et cetera, et cetera. That is, if you take the gold from the table, you just initiate a war.
In the best case, this could raise the gas price and congest the blockchain for a while. If miners join the war, they could even shuffle transactions or reorder blocks to make sure their transactions front-run everyone else. And that would introduce catastrophic chaos to the blockchain.
Basically, the solution is to conduct it secretly.

Instead of publishing the transaction to the public mempool, you bypass it and send your transaction (or bundle of transactions) direct to the miners through the Flashbots private transaction pool.
const provider = new ethers.providers.JsonRpcProvider({ url: API_URL });
const searcher = new ethers.Wallet(SEARCHER_PRIVATE_KEY, provider);
const authSigner = new ethers.Wallet(AUTH_SIGNER_PRIVATE_KEY, provider);
const flashbotsProvider = await FlashbotsBundleProvider.create(provider, authSigner);
const tx = {
'from': SEARCHER_PUBLIC_KEY,
'to': contractAddress,
'gasLimit': 40000,
'gasPrice': gasPrice,
'chainId': 1,
'data': buildData(), // contract interaction
};
const dummyTx = {
'from': SEARCHER_PUBLIC_KEY,
'to': SEARCHER_PUBLIC_KEY,
'value': 20000000000000, // 0.00002 Ether
'gasLimit': 21000,
'gasPrice': gasPrice,
'chainId': 1,
};
const signedTransactions = await flashbotsProvider.signBundle([
{
signer: searcher,
transaction: tx,
},
{
signer: searcher,
transaction: dummyTx,
}
]);
const blockNumber = await provider.getBlockNumber();
const bundleSubmission = await flashbotsProvider.sendRawBundle(signedTransactions, blockNumber + 1);
provideris the regular Web3 provider, you can use Infura, Alchemy, etc.searcheris your address, because you search the MEV.authSigneris a signer for the Flashbots provider, you don’t need to put any Ether in this address.flashbotsProvideris the provider through which you talk to the Flashbots network.I create a dummy transaction to send myself some Ether because the Flashbot bundle requires a minimum gas limit of 42000.

Under the protection of Flashbots, I successfully walked out of the Dark Forest.
[1] https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest
[2] https://samczsun.com/escaping-the-dark-forest/
[3] https://docs.flashbots.net/
[4] The transaction

