This post discusses the closely tied issues of front-running and transaction ordering attacks on the ETH blockchain. It’s comprised of an explanation of the problem, and a list of solutions it with explanations. Feel free to comment with anything relevant, I’d love to start a conversation about this stuff.
Without further ado…
What is Front-Running?
In the days when stocks were actually traded on paper, on the floor of the stock exchange, front-running referred to the practice of running to the front of the line when you knew some big trade was coming (clever name, right?). For instance, if you knew someone was about to buy a huge amount of some stock, you could buy it before them, then sell it at a higher market price after their large buy went through.
On Ethereum the concept isn’t much different. Front-runners wait for transactions in the mempool (the pool of unconfirmed transactions) that give them some useful information, like a big, juicy buy order on some decentralized exchange. They then broadcast their own transaction, which also goes to the mempool, but they try to make sure that it makes it into the blockchain before the other transaction in order to profit. However, the scope of Ethereum front-running is wider than just manipulating order books on exchanges.
Unfortunately, it seems that front-running, or more generally, transaction re-ordering, is inherent to the structure of a blockchain. For one thing, distributed networks imply some degree of latency. This means that if you’re broadcasting your transaction to a miner in China from the USA, and someone in Russia broadcasts a transaction 1 second later, the Russian’s transaction may reach the miner before yours, even though you sent yours first! Alas, the world of blockchain can sometimes be unfair. And, as much as we here in crypto-land love to espouse “trustlessness”, we DO trust a certain group of users - the miners. We trust miners to put our transactions into blocks in a fair, predictable way. Occasionally they won’t, because of latency issues, network congestion, or perhaps a personal vendetta. But they have been shown to manipulate transaction order for their own personal gain. All of these mechanisms create different ins for front-running.
In this article, we’ll refer to “front-running” as the action of manipulating the order that transactions are placed into, or leveraging information in the mempool, to achieve some malicious effect.
Constructing Front-running attacks
Front-running attacks are generally based on the order of transactions. If you can recognize an instance where changing the order of transaction’s unfairly benefits one party, and where the transactions in question may end up in the same block, you’ve got a potential front-running vulnerability.
Any information that is sent in a transaction should be considered public once it is sent (that includes the amount of ether you send, as well as your transactions recipient address!). Also understand that there is some latency between when a transaction is sent and when or if it is confirmed. A front-runner could read your transaction before it is confirmed, and place one before yours that is based on your transaction’s information. If you’re about to send an Ethereum transaction, you must consider whether it would be bad if someone very smart had ~15 seconds (one block-time) to jump in front of you. This “15 second grace period” arises from users sending transactions within the same block as you, or from a miner intentionally leaving your transaction out of a block (requiring you to wait at least 15 seconds for the next block). At times when network congestion is high, your transaction may not be mined and may sit in the mempool for even longer than 1 block, giving front-runners more time to act on it.
How to Counter Front-running
Transaction Counter
In many, if not most cases, the attacker’s aim is to inconspicuously send a transaction before the victim’s transaction is executed. So, to prevent this, you can utilize a transaction counter within your smart contract. Whenever a state-modifying transaction occurs in your smart contract, increment a universal transaction counter by one. When sending a transaction that you believe could be front-run in a typical scenario, you also will send a transactionCount
value which dictates what the transaction counter’s value should be when your transaction is initiated. If the transaction counter’s value is NOT equal to the value you’ve specified, the transaction reverts.
Credits to Chris Cloverdale at Coinmonks for this neat solution.
As a modification of this idea, you could also set it up so that certain sections of your smart contract have different transaction counters. This way, only some state changes have a bearing on whether specific transactions can go through. An “auction” smart contract could have multiple auctions, each with their own transaction counters, for instance.
This solution works well for many purposes, but of course has the side effect that if any other transactions increment the counter before yours, yours may revert. If the intended functionality of your smart contract is to enable multiple potential transaction orders, this method may be more complicated to implement and may frustrate users as their transactions are reverted.
Gas Price Limiting
This method to mitigate front-running also requires very little overhead (which saves your users on gas-costs). In Solidity, one could make a modifier
called gasThrottle
, which checks that the gas cost for the transaction making the function call (via tx.gasPrice
) is less than or equal to some amount which we’ll call MAX_GAS_PRICE
.
This somewhat prevents people from seeking preferential treatment from miners by using a higher gas cost. It actually still allows them to jump ahead in line, but limits how much they can push to do so.
Compared to the above solution of using transaction counters, this solution allows transactions to happen in any order, without having to worry about transaction reverts.
The problem with this strategy is that it needs to be supervised… forever. Gas costs on Ethereum are highly variable. What might be a reasonably “high” gas cost today may be far, far too low/high a month from now. If your MAX_GAS_PRICE
limiter is too low, your dApp may be frozen, as no transactions with such a gas price will be accepted by miners on the network.
Additionally, malicious miners can still choose to not order transactions by gas price, perhaps in order to perform front-running. This is in contrast to the transaction counter solution, which will programmatically not allow certain transactions to happen in a certain order.
It should be re-stated that this is a relatively weak solution to the front-running issue.
Off-Chain Ordering Solutions
For the sake of brevity, I will not delve deeply into off-chain ordering mechanics. There are simply too many different ways to handle off-chain settlement, and they are not within areas that I am necessarily knowledgeable about. But, we can generalize the concepts here.
Off-chain ordering implies two steps: ordering (done, of course, not on the blockchain) and settlement (done on the blockchain). Whoever is implementing the ordering solution has the liberty of choosing from a huge list of potential platforms to build on. They can leverage features of those systems that blockchains may not have, such as time stamped messages, free messages, and higher throughput. Money can be saved on transaction costs as well, as all that is needed is a net change in affected state variables per batch of transactions, rather than individual changes for each transaction. Unlike the other options explored here, it is possible to implement an off-chain ordering protocol that updates order books every block (or faster! Of course, that won’t be reflected on-chain). But, in my opinion, the greatest benefit to an off-chain ordering solution is that it has the potential to cut down its Ethereum use to the bare essentials, which helps keep the network de-congested makes the blockchain more usable for everyone on it.
The downsides of a system like this is that it is less transparent and tends to have a higher degree of centralization, which are both things that blockchain enthusiasts dislike. Your actions may not be provable or publicly visible in a system like this. Updates to outside software can also be problematic, and by invoking an off-chain solution you’ll have a more complex software implementation to manage. Unpredictable interoperability bugs gain their foothold here.
For an example of an off-chain ordering system, check out this article about 0x.
Intermediate States
This strategy is a tricky one to describe generally, but it relies on limiting the potential damages that can be caused by having arbitrary transaction order through the use of intermediate states. The thinking is that if you can spread the logic out over multiple blocks or transactions, you can better ensure that there’s no dispute over the order of transactions or on what the expected outcome is. Also, whenever a security issue occurs, having intermediate states helps to more clearly identify what the heck is happening and to contain the issue.
Consider the case where you have multiple admins of some dApp. The admins all have the power to remove one another from adminship, in case one of them goes rogue. One day, one admin (Bob) is misbehaving, and another admin (Alan) sends a transaction to remove Bob’s admin powers. But Bob is clever, and Bob sends a “remove Alan” transaction with a higher gas price around the same time. Bob’s transaction is accepted first, and Alan is deposed. Alan’s transaction will be refused, because he is no longer an admin. Bad Bob the evil admin has won, and Alan is powerless to change this fact.
But, imagine if a “flag admin” function existed. When one admin calls “flag admin” on another admin, they both are placed into some sort of probation state. The probation state takes away all of the two admins’ powers, and initiates a vote from all other admins to decide whether or not to remove them. After the vote, each admin is either removed from adminhood or re-instated. The probationary state is an intermediate state between admin and not-admin.
This solution is, again, difficult to generalize, but the heart of it is this: no matter what order the competing transactions are in, the same thing will happen.
Compared to limiting gas prices, this solution may require more overhead cost for users and developers alike. Your contract logic is likely to be more complex, which can impart a larger surface for attack. Your users may need to send more than one transaction to complete a desired action, which costs them more in both ETH and in time. This has the added effect of meaning that single actions on your dApp may take longer than similar actions on other dApps.
Finally, incentive mechanisms may be more complex, in order to incentivize users to finish their actions. In the above example, you wouldn’t want your dApp being stalled out, waiting for some lazy admin to cast their vote! You’ll want to either design the voting scheme so it doesn’t depend on everyone voting, or you’ll want to incentivize voters to vote quickly. Consider what will drive users to push the contract back out of the intermediate state.
Submarine Sends, or Commit-Reveal strategies
Submarine sends (a feature of LibSubmarine) are a concept based on research done by IC3. The basic concept of submarine sends is that all transaction data is encrypted, and the amount of ether sent with them has the potential to be greater than the amount of ether actually desired for the transaction. Additionally, transactions are first committed to a “submarine address”, thus obscuring the final destination of the transaction until after the reveal. They aim for full obscurity of transaction parameters, ether amount, and destination. Once the encrypted transaction is mined into a block, its owner can choose to reveal it later, after which the transaction is executed as intended.
In the case of a sealed-bid auction, submarine sends could be incredibly useful. They could allow you to hide your bid until the bidding period is over, then reveal it afterwards. This could also be leveraged on DEXs, to ensure that people can’t tell what or how much you’re trading, making it hard to “front-run” in the stock-exchange sense.
The downside of submarine sends is that they take longer to process, and therefore effectively lower the blocktime of your dApp. In the best case, your transactions would be in block x
(the commit transaction) and block x+1
(the reveal transaction), but more likely they will be spread out across more than two blocks.
It should be pointed out also that submarine sends are still vulnerable to information-leaks in the reveal stage, and miners can subsequently censor reveal transactions that they don’t like.
Submarine sends are essentially a beefed-up commit-reveal strategy, with many cleverly designed features to make the scheme generally more useful for Ethereum applications. Otherwise, typical commit-reveal schemes are simple to implement, but afford less features than submarine sends, namely the use of a “submarine address” and the recording of a commit block number.
With basic commit-reveal strategies, keep in mind that you can include as much information or as little information as you’d like. Information like the block number, a salt, or a cryptographic signature from the sender can be used for ordering, obscuring information, and identity verification, respectively.
Injective Protocol
The Injective Protocol is a recently funded venture by Binance Labs. The basic concept of the injective protocol is that users must provide “proof of elapsed time” by solving verifiable delay functions (VDF’s). The injective protocol is designed for decentralized exchanges, in which order “takers” may have to deal with front-running frequently.
As per the protocol, when a user has decided they’d like to “take” an order, they begin solving a VDF. The longer they spend solving this function, the higher t value
they have. At the end of a round, whoever has the highest t value
(with a correct proof of their solution) wins the privilege of taking the order. The idea is that whoever started the process of solving the VDF first will have the highest t value
, even with potential differences in processor speeds.
The project is still in-the-works, but it’s a very fascinating concept. The use of VDF’s may be able to solve the arbitrary ordering issue inherent to most blockchains. However, this solution also does require more overhead in terms of the user’s compute resources, and, like almost every solution mentioned here, it increases the complexity of contract logic. Like submarine sends, the whole process takes more than 1 block per round, meaning slower applications. Finally, it may be possible to develop efficient enough VDF solvers that certain users with more money to spend on fancy hardware gain an unfair advantage (Think ASIC’s, but for solving VDF’s). Of course this is a primary concern to the Injective Protocol developers, and they’re designing around it, but the future is still unpredictable.
Randomizing order (Theoretical)
This is an idea that I’ve been toying with recently. The general premise is that after transactions are committed (in a commit-reveal scheme), their order is randomized within a given round. The ordering is then enforced by contract logic. Essentially, there’s no guarantee for front-runners to actually be at the front of the line. Everyone is on a level playing field, and miners cannot manipulate transaction order once it’s been set since it’s written into a smart contract.
The benefit of a solution like this is that it could be implemented completely on-chain, if that’s desired, and that it would help to mitigate issues caused by latency. Even if two users submit at the same time, one who’s physically closer to a node might typically have their transaction mined first. This implementation lowers the chance of this occurring.
Unfortunately, as much as I may want it to be, this solution is not perfect. The logical overhead for ordering the transactions means that gas costs for transactions are increased. Additionally, would-be front-runners could spam the same transaction via multiple accounts, effectively increasing their chances of one of them being first (if a user submits 99 of the total 100 transactions, ONE of them will be almost definitely be first). Finally, the chances of getting put last in the order is a risk that everyone must accept with equal probability. Even the good, honest user who submits his transaction first may risk being placed last in line!
Since this concept is theoretical, I’m all ears for any comments or questions about it.
That’s it, thanks for reading!