Race Conditions/Front Running
The combination of external calls to other contracts and the multiuser nature of the underlying blockchain gives rise to a variety of potential Solidity pitfalls whereby users race code execution to obtain unexpected states. Reentrancy (discussed earlier in this chapter) is one example of such a race condition. In this section we will discuss other kinds of race conditions that can occur on the Ethereum blockchain. There are a variety of good posts on this subject, including “Race Conditions” on the Ethereum Wiki, #7 on the DASP Top10 of 2018, and the Ethereum Smart Contract Best Practices.
The Vulnerability
As with most blockchains, Ethereum nodes pool transactions and form them into blocks. The transactions are only considered valid once a miner has solved a consensus mechanism (currently Ethash PoW for Ethereum). The miner who solves the block also chooses which transactions from the pool will be included in the block, typically ordered by the gasPrice
of each transaction. Here is a potential attack vector. An attacker can watch the transaction pool for transactions that may contain solutions to problems, and modify or revoke the solver’s permissions or change state in a contract detrimentally to the solver. The attacker can then get the data from this transaction and create a transaction of their own with a higher gasPrice
so their transaction is included in a block before the original.
Let’s see how this could work with a simple example. Consider the contract shown in FindThisHash.sol.
Example 10. FindThisHash.sol
contract FindThisHash {
bytes32 constant public hash =
0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() external payable {} // load with ether
function solve(string solution) public {
// If you can find the pre-image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
Say this contract contains 1,000 ether. The user who can find the preimage of the following SHA-3 hash:
0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a
can submit the solution and retrieve the 1,000 ether. Let’s say one user figures out the solution is Ethereum!
. They call solve
with Ethereum!
as the parameter. Unfortunately, an attacker has been clever enough to watch the transaction pool for anyone submitting a solution. They see this solution, check its validity, and then submit an equivalent transaction with a much higher gasPrice
than the original transaction. The miner who solves the block will likely give the attacker preference due to the higher gasPrice
, and mine their transaction before the original solver’s. The attacker will take the 1,000 ether, and the user who solved the problem will get nothing. Keep in mind that in this type of “front-running” vulnerability, miners are uniquely incentivized to run the attacks themselves (or can be bribed to run these attacks with extravagant fees). The possibility of the attacker being a miner themselves should not be underestimated.
Preventative Techniques
There are two classes of actor who can perform these kinds of front-running attacks: users (who modify the gasPrice
of their transactions) and miners themselves (who can reorder the transactions in a block how they see fit). A contract that is vulnerable to the first class (users) is significantly worse off than one vulnerable to the second (miners), as miners can only perform the attack when they solve a block, which is unlikely for any individual miner targeting a specific block. Here we’ll list a few mitigation measures relative to both classes of attackers.
One method is to place an upper bound on the gasPrice
. This prevents users from increasing the gasPrice
and getting preferential transaction ordering beyond the upper bound. This measure only guards against the first class of attackers (arbitrary users). Miners in this scenario can still attack the contract, as they can order the transactions in their block however they like, regardless of gas price.
A more robust method is to use a commit–reveal scheme. Such a scheme dictates that users send transactions with hidden information (typically a hash). After the transaction has been included in a block, the user sends a transaction revealing the data that was sent (the reveal phase). This method prevents both miners and users from front-running transactions, as they cannot determine the contents of the transaction. This method, however, cannot conceal the transaction value (which in some cases is the valuable information that needs to be hidden). The ENS smart contract allowed users to send transactions whose committed data included the amount of ether they were willing to spend. Users could then send transactions of arbitrary value. During the reveal phase, users were refunded the difference between the amount sent in the transaction and the amount they were willing to spend.
A further suggestion by Lorenz Breidenbach, Phil Daian, Ari Juels, and Florian Tramèr is to use “submarine sends”. An efficient implementation of this idea requires the CREATE2
opcode, which currently hasn’t been adopted but seems likely to be in upcoming hard forks.
Real-World Examples: ERC20 and Bancor
The ERC20 standard is quite well-known for building tokens on Ethereum. This standard has a potential front-running vulnerability that comes about due to the approve
function. Mikhail Vladimirov and Dmitry Khovratovich have written a good explanation of this vulnerability (and ways to mitigate the attack).
The standard specifies the approve
function as:
function approve(address _spender, uint256 _value) returns (bool success)
This function allows a user to permit other users to transfer tokens on their behalf. The front-running vulnerability occurs in the scenario where a user Alice approves her friend Bob to spend 100 tokens. Alice later decides that she wants to revoke Bob’s approval to spend, say, 100 tokens, so she creates a transaction that sets Bob’s allocation to 50 tokens. Bob, who has been carefully watching the chain, sees this transaction and builds a transaction of his own spending the 100 tokens. He puts a higher gasPrice
on his transaction than Alice’s, so gets his transaction prioritized over hers. Some implementations of approve
would allow Bob to transfer his 100 tokens and then, when Alice’s transaction is committed, reset Bob’s approval to 50 tokens, in effect giving Bob access to 150 tokens.
Another prominent real-world example is Bancor. Ivan Bogatyy and his team documented a profitable attack on the initial Bancor implementation. His blog post and DevCon3 talk discuss in detail how this was done. Essentially, prices of tokens are determined based on transaction value; users can watch the transaction pool for Bancor transactions and front-run them to profit from the price differences. This attack has been addressed by the Bancor team.