Skip to main content
Sigvex

Front-Running

Detects transactions where the outcome depends on call ordering, enabling miners or other users to manipulate execution order for profit.

Front-Running

Overview

Remediation Guide: How to Fix Front-Running

The front-running detector identifies smart contract patterns where the outcome of a transaction is predictable from the mempool and can be exploited by an observer who submits a competing transaction with a higher gas price. This includes: DEX arbitrage without slippage protection, commit-reveal schemes that reveal secrets prematurely, ERC-20 approve() followed by transferFrom() without incremental allowance patterns, and any function that relies on being the first caller to succeed.

Sigvex detects potential front-running by analyzing function signatures and data-flow patterns that historically correlate with ordering sensitivity: first-mover rewards, dutch auctions without time locks, token purchases with no price limits, and exposed commitment values in function parameters.

Why This Is an Issue

Front-running is endemic in Ethereum’s mempool. MEV (Maximal Extractable Value) bots continuously monitor pending transactions and submit competing transactions to capture arbitrage. While not all front-running causes direct harm, it can lead to: failed arbitrage for legitimate users, stolen whitelist spots in NFT mints, pre-empted liquidations, sandwich attacks on DEX trades, and stolen protocol bounties.

The approve-race detector covers the specific ERC-20 approve/transferFrom race condition. This detector covers broader ordering-sensitive patterns.

How to Resolve

// Before: Vulnerable DEX swap — no slippage or deadline
function swapTokens(uint256 amountIn, address tokenIn, address tokenOut) external {
    // No minAmountOut or deadline — sandwich attack possible
    uint256 amountOut = calculateOutput(amountIn, tokenIn, tokenOut);
    _executeSwap(tokenIn, tokenOut, amountIn, amountOut);
}

// After: Slippage protection and deadline
function swapTokens(
    uint256 amountIn,
    uint256 minAmountOut,  // Minimum acceptable output
    address tokenIn,
    address tokenOut,
    uint256 deadline       // Transaction must be included by this time
) external {
    require(block.timestamp <= deadline, "Swap expired");
    uint256 amountOut = calculateOutput(amountIn, tokenIn, tokenOut);
    require(amountOut >= minAmountOut, "Insufficient output amount");
    _executeSwap(tokenIn, tokenOut, amountIn, amountOut);
}

For commit-reveal to prevent front-running of revealed secrets:

contract CommitReveal {
    mapping(address => bytes32) public commitments;
    uint256 public revealStart;

    // Phase 1: commit (hash is kept secret until reveal phase)
    function commit(bytes32 hashedValue) external {
        commitments[msg.sender] = hashedValue;
    }

    // Phase 2: reveal only after commit phase ends
    function reveal(uint256 secret) external {
        require(block.timestamp >= revealStart, "Not yet reveal phase");
        bytes32 expected = keccak256(abi.encodePacked(secret, msg.sender));
        require(commitments[msg.sender] == expected, "Invalid reveal");
        // Process revealed value
    }
}

Examples

Vulnerable Code

// ERC-20 approve then transferFrom — front-running attack possible
contract VulnerableToken is IERC20 {
    function approve(address spender, uint256 amount) external returns (bool) {
        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }
    // Attack: owner sets allowance from 100 to 50
    // Spender front-runs and spends 100 before the tx confirms
    // Then spender also gets to spend 50 — total 150 extracted
}

Fixed Code

contract SafeToken {
    // Use increaseAllowance / decreaseAllowance instead of absolute approve
    function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {
        _approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue);
        return true;
    }

    function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {
        uint256 currentAllowance = _allowances[msg.sender][spender];
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(msg.sender, spender, currentAllowance - subtractedValue);
        }
        return true;
    }
}

Sample Sigvex Output

{
  "detector_id": "front-running",
  "severity": "medium",
  "confidence": 0.65,
  "description": "Function swapTokens() performs a DEX swap without a minimum output amount or deadline parameter. Searchers can sandwich this transaction to extract value.",
  "location": { "function": "swapTokens(uint256,address,address)", "offset": 92 }
}

Detection Methodology

  1. Pattern matching: Identifies function signatures commonly associated with front-running vulnerability (swap, approve, bid, mint, claim).
  2. Slippage parameter analysis: Checks whether swap-like functions include a minOut or minAmountOut parameter that is validated.
  3. Deadline parameter analysis: Verifies that time-sensitive operations include a deadline compared against block.timestamp.
  4. Commitment exposure: Looks for patterns where a function accepts a plaintext value that will be used in a first-mover context.

Limitations

False positives:

  • Internal/private functions that are only reachable through a protected external function may be flagged.
  • Protocols that implement their own sandwich protection through relays (e.g., Flashbots Protect) are not recognized.

False negatives:

  • Multi-step MEV strategies involving chained transactions are not fully modeled.
  • Governance front-running (placing votes before a critical proposal) is a separate concern.

References