Skip to content

Frontrunning

Every transaction on blockchain networks undergoes a period of visibility in the mempool before execution. Such transparency allows network participants to see and respond to a transaction before its inclusion in a block. This information leak can be leveraged by attackers to influence the course of a transaction's execution.

A potential exploit scenario could involve a decentralized exchange where a visible buy order transaction could be preempted by broadcasting and executing another transaction before the initial one is included in the block.

Protecting against frontrunning attacks is not trivial because they are often tailored to the target code base. To facilitate discussion about the issue and identify potential solutions, frontrunning attacks can be classified according to a taxonomy into three categories: Displacement, Insertion, and Suppression.

Displacement

In a displacement attack, the attacker relegates the user's transaction to a lower position in the block. The user's function may be orphaned, executed without causing significant effect, or reverted due to running on an outdated state. Such an attack occurs when the gas price is amplified to exceed the original transaction's value, often by ten times or more. This simple number-guessing game underlines the issue of a frontrunning attack using the displacement technique:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.8.17;

contract GuessTheNumberChallenge {
    bytes32 challenge;

    constructor(bytes32 _challenge) payable {
        require(msg.value == 1 ether);
        challenge = _challenge;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint256 number) public payable {
        require(msg.value == 1 ether, "Submission fee required");
        uint256 balance = address(this).balance;
        require(balance != 0, "Game has ended");

        bytes32 userChallenge = keccak256(abi.encode(number));
        if (userChallenge == challenge) {
            (bool success, ) = msg.sender.call{value: balance}("");
            require(success, "Transfer failed");
        }
    }
}

The game's premise is simple: a player must guess a particular number that has been hashed and stored within the contract. Initially, the contract's deployer submits 1 ETH in the constructor, along with the hash of the challenge. The challenge hash is derived from a number calculated off-chain by the deployer. The pre-image of this hash constitutes the secret that players aim to guess. If a user can correctly guess the hashed number, the guess function will generate a hash that matches the original challenge. In this case, the contract will pay out its entire balance to the user as a reward.

A fundamental design flaw exists within this system. When a user is ready to submit their guess, they call the guess function and pass their number as a parameter. Unless special precautions are taken, this transaction becomes visible in the mempool before it is mined and included in a block. This visibility presents an opportunity for any observer to copy the transaction data, which includes the guess function signature and its parameters.

Generalized frontrunners can copy this transaction data, simulating the transaction on a forked network. If the transaction results in a profit, they can resubmit the copied payload with a gas price higher than the original user's. This outbidding in the gas price auction increases the chances that their transaction will be mined before the original user's transaction.

This scenario reflects an essential aspect of displacement frontrunning. Once the copied transaction data has been submitted and mined, the original transaction is rendered irrelevant within the smart contract system's context. Specifically, in the case of this guessing game, the contract checks for a non-zero balance before executing the guess function. However, once a correct guess transaction (even a copied one) has been mined, the contract's balance goes to zero, ending the game. Thus, any subsequent transactions calling the guess function will fail due to this balance check. The user who had originally guessed the correct number will find their transaction reverted, despite having been the rightful winner of the game.

Insertion

In an insertion frontrunning attack, the successful execution of the original user's function call after the adversary's transaction becomes imperative. After executing their transaction, the adversary modifies the contract state, necessitating the execution of the user's initial function within this altered context.

A prime example of this kind of attack is slippage skimming. When a user bids on a decentralized exchange to acquire an asset, they offer an asking price and a specified slippage range (e.g., 3%) for allowable price deviation. The adversary can strategically manipulate this setup by inserting two transactions. Firstly, before the user's trade, the adversary purchases the same asset, causing the asset price to inflate. This hike corresponds to the sub of the user's asking price and the maximum allowed slippage amount.

Consequently, the user's transaction executes at the peak feasible price. Lastly, the adversary triggers the second transaction post the user's trade, offloading the assets, which results in a deflation of the asset price. In the aftermath, the adversary yields a profit equivalent to the user's maximum slippage, exploiting the price difference. This amount can be significant, especially for large order sizes.

This attack is often termed a sandwiching attack due to the placement of the user's transaction between two adversarial transactions. Positioning a transaction after the original one is also called backrunning.

Suppression

A malicious actor can temporarily delay the execution of other transactions in the blockchain by launching what is known as a Block Stuffing or suppression attack. This strategy involves issuing a series of transactions with a high gas price, preventing other transactions from being processed for multiple blocks.

A suppression attack targets the delay of a transaction's execution. This attack has been demonstrated in cases like the Fomo3d game-winner, which prevented transactions after the adversary's transaction. The attacker generates multiple transactions with high gasPrice and gasLimit, directing them to custom smart contracts. This action, for instance, via failing assert statements, consumes all the available gas, thereby filling up the block's gas limit.

The issue of block gas exhaustion, a technique often used in these attacks, has been addressed by introducing Ethereum Improvement Proposal (EIP) 1559. This proposal alters the way Ethereum transaction fees work, introducing a mechanism to burn the base fee, thereby decreasing the incentive for miners to manipulate the block gas limit.

However, the risk of block stuffing attacks has not been entirely eliminated. Malicious actors can still spam transactions with a high gas price, thereby consuming a significant portion of the block's capacity and delaying other transactions.

These attacks become particularly devastating when they target crucial transactions, such as oracle updates. Oracles often provide vital price information for various assets in the decentralized finance ecosystem. A delay in these updates due to block stuffing attacks can lead to significant market inefficiencies, inaccurate valuations, and even market manipulation, posing considerable risk to the stability and security of the entire decentralized finance infrastructure.

Mitigation Strategies

Frontrunning is a widespread problem on public blockchains such as Ethereum. One primary remediation strategy is the removal of frontrunning benefits in applications, mainly by minimizing the relevance of transaction ordering or timing. Implementing batch auctions could offer a solution for markets, providing protection against high-frequency trading concerns. A pre-commit scheme, with later submitted details, represents another mitigation approach. Also, defining a maximum or minimum acceptable price range on a trade can constrain price slippage, reducing the cost of frontrunning.

Another common strategy could be restricting the visibility of transactions using a "commit and reveal" scheme. An elementary implementation involves storing the Keccak256 hash of the data in the first transaction, then revealing the data and verifying it against the hash in the second transaction. The transaction could leak intention and collateralization value. More secure but complex commit-reveal schemes, such as submarine sends, require additional transactions for operation.