//Money Trail
>TL;DR
- Start from the 100k FLOW deposit into
0xfb1e691adac8361ba054626733bc4b701568da8athat predates the CTF launch and recursively follow every outbound hop on Flow's EVM testnet. - Build a breadth-first crawler around Flowscan's
account.txlistendpoint to enumerate every descendant externally owned account (EOA) and collect transactions whose calldata is non-empty. - The traversal uncovers exactly one contract-style transaction:
0x632fe7300bbcace53a84282ca415708cabe502d0dd9307ea7886742f5ba1c1df, where theinputblob hex-decodes to the hidden flagnite{6o0d_J0b_n0w_Tr4c3_mY_m0n3r0}.
>1. Challenge Brief
- Network: Flow EVM Testnet (testnet.evm.nodes.onflow.org RPC / evm-testnet.flowscan.io explorer API)
- Premise: Someone laundered funds through a daisy chain of EOAs before the CTF went live; our job is to trace the path and surface any "sus messages" embedded in contract calls along the way.
- Constraints: Stay within the pre-CTF block range, prefer open data sources (Flowscan REST API + public JSON-RPC), and produce a reproducible audit trail suitable for contest judging.
>2. Manual Recon and Baseline Data
-
Seed transaction: Pull the saver transaction for the Flow-provided hash
0x9ba8…via Blockscout and confirm that0xfb1e…da8areceives exactly 100,000 FLOW. -
Handshake check: Query that address through Flowscan's REST API to dump its chronological history: This shows early fan-out transactions toward EOAs such as
0x538e…,0x6ec5…,0x7e79…,0x60ca…, and0xec46….bashcurl "<https://evm-testnet.flowscan.io/api?module=account&action=txlist&address=0xfb1e691adac8361ba054626733bc4b701568da8a&sort=asc>" -
Sanity filter: Because each hop is a plain value transfer, we log every recipient, track timestamps (all <
2024-12-01T00:00:00Zto satisfy the "before CTF" rule), and note that no calldata is attached yet. At this point, doing the remainder manually would be tedious, so automation is warranted.
>3. Automating the Crawl
To ensure every downstream hop is inspected, I wrote a BFS crawler in trace_flow.py:
- Queue-driven traversal: Start from the seed EOA, pop addresses one at a time, and push any
toaddress observed in outbound transactions. - Dedup + halt: Maintain a
visitedset to avoid loops; stop when the queue empties, meaning all reachable EOAs (from that provenance chain) have been explored. - Signal extraction: For every transaction whose
fromequals the current node, copy it into acontract_callslist whenever theinputfield is anything other than0x. - Resilience: Wrap the Flowscan API call in a 5-attempt exponential back-off so intermittent 429/5xx responses do not derail the crawl, and print compact JSON stats every 25 nodes to monitor progress.
Key portions of the script (abridged for readability):
API = "<https://evm-testnet.flowscan.io/api>"
START_ADDRESS = "0xfb1e691adac8361ba054626733bc4b701568da8a"
queue = deque([START_ADDRESS])
visited: Set[str] = set()
contract_calls: List[Dict[str, str]] = []
while queue:
addr = queue.popleft().lower()
if addr in visited:
continue
visited.add(addr)
txs = fetch_transactions(addr)
if txs is None:
continue
for tx in txs:
if tx.get("from", "").lower() != addr:
continue
if tx.get("input", "0x") not in ("0x", "0X"):
contract_calls.append(tx)
if tx.get("to"):
queue.append(tx["to"].lower())
Execution
Run the crawler inside the project virtualenv:
cd /home/noigel/Desktop/next_hunt/ByteDoubleCross
. .venv/bin/activate
python trace_flow.py
Progress snapshots (emitted every 25 processed addresses) show both queue depth and discovery of contract calls:
{"processed": 200, "queue_size": 231, "visited": 200, "contract_calls_found": 0}
...
{"processed": 275, "queue_size": 128, "visited": 275, "contract_calls_found": 1}
After 350 nodes the crawl completes with 367 unique EOAs visited and exactly one transaction containing calldata.
>4. Surfacing the Suspicious Transaction
The crawler's final JSON payload highlights the lone contract-style interaction:
{
"blockNumber": "83335968",
"from": "0x00758cf756f8834da912d9f5cc9cf801d013515f",
"to": "0x00758cf756f8834da912d9f5cc9cf801d013515f",
"hash": "0x632fe7300bbcace53a84282ca415708cabe502d0dd9307ea7886742f5ba1c1df",
"input": "0x6e6974657b366f30645f4a30625f6e30775f54723463335f6d595f6d306e3372307d",
"value": "0",
"gasUsed": "22360",
"timeStamp": "1765280704"
}
Noteworthy observations:
- It is a self-call (
from == to), so no new EOA enters the crawl, which explains why only one contract call surfaced. - Gas usage (
22360) indicates the transaction executed logic rather than a bare transfer, reinforcing that the payload might hide a message. - The timestamp
1765280704converts to2025-11-11 05:25:04 UTC, still before the challenge launch window, satisfying the "historic trail" requirement.
Cross-verify via RPC if desired:
curl -H "Content-Type: application/json" \\
-d '{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["0x632fe7300bbcace53a84282ca415708cabe502d0dd9307ea7886742f5ba1c1df"],"id":1}' \\
<https://testnet.evm.nodes.onflow.org>
>5. Decoding the Payload
The input field is short and lacks function selectors, so treating it as ASCII hex is the natural next step. A single Python oneliner does the trick:
python - <<'PY'
data = "6e6974657b366f30645f4a30625f6e30775f54723463335f6d595f6d306e3372307d"
print(bytes.fromhex(data).decode())
PY
Output:
nite{6o0d_J0b_n0w_Tr4c3_mY_m0n3r0}
No further decoding is required—the author literally embedded the flag as plain ASCII once the entire laundering tree had been enumerated.