Analytical reporting · Lesson 6 of 6
Case study: an end-to-end investigation
10 min read
The previous 41 lessons each covered one skill. Real investigations use them all at once, under time pressure, with incomplete data and readers waiting for answers. This case follows a fictional exploit from the first alert to the final report, showing at each step which skill is used and how the analyst decides how much to trust each result. Every name, address, hash, block height and amount in it is invented.
The lead and the question
At 09:58 UTC on 14 March 2026, Corvane Finance, a fictional lending protocol on Ethereum, sees its USDC market drained. Its team sends an independent analytics team the hash of the exploit transaction, 0x9c4e…a71b, and a one-line request: "Track the money."
The analyst rewrites it as an analytic question, as described in the lesson From question to hypothesis:
Where did the assets drained in transaction 0x9c4e…a71b go, up to the cut-off blocks; which custodial services received any of them; and how confident are we in each link?
The scope statement fixes the seeds (the exploit transaction), the chains (Ethereum, one rollup, Bitcoin), the window (Ethereum blocks 24,790,112 to 24,812,000 and the matching Bitcoin range), a 10-hop limit for Bitcoin peel chains, and a stop rule at any custodial service. It is written into the case file before the first query runs.
Step 1: Reading the exploit transaction
The analyst pulls the transaction, its receipt and its traces from an archive node (see Node APIs and data access) and records the block hash for provenance. The receipt's status is 1 (success), and the decoded logs show what left the protocol:
{
"transactionHash": "0x9c4e…a71b",
"blockNumber": 24790112,
"status": "0x1",
"decodedLogs": [
{"token": "USDC", "event": "Transfer", "from": "0x5Fd0…3C2a",
"to": "0x7aE3…b19C", "value": "1912400000000"},
{"token": "WETH", "event": "Transfer", "from": "0x5Fd0…3C2a",
"to": "0x7aE3…b19C", "value": "210000000000000000000"}
]
}
0x5Fd0…3C2a is the Corvane USDC market and 0x7aE3…b19C the attacker's contract. Normalizing units (see Data quality, normalization and provenance): USDC has 6 decimals, so 1,912,400,000,000 is 1,912,400 USDC; WETH has 18, so the second value is 210 WETH. The traces show a flash loan, a price manipulation in a thin pool and the drain, the classic anatomy covered in the lesson Analyzing exploits and hacks. A later transaction moves everything from the contract to the deployer, the externally owned account 0xE7c1…44aD.
That account was created two days earlier: its first incoming transfer, received before it had sent any transaction (nonce still 0), was 1.0 ETH withdrawn from a smart-contract mixer. As the lesson Mixers, CoinJoin and obfuscation in graphs explains, that funding tells us almost nothing about who is behind it, and the analyst records it as a dead end rather than a lead.
Step 2: Token flows in SQL
To find every token movement out of the attacker's addresses, the analyst queries raw logs by event signature, as in the lesson Querying EVM logs and traces:
-- ERC-20 transfers sent by the exploit contract or the exploiter account
SELECT
l.block_number,
l.transaction_hash,
l.contract_address AS token,
'0x' || SUBSTR(l.topic2, 27, 40) AS recipient,
l.data AS raw_amount_hex
FROM logs l
WHERE l.block_number BETWEEN 24790112 AND 24812000
AND l.topic0 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
AND SUBSTR(l.topic1, 27, 40) IN ('7ae3…b19c', 'e7c1…44ad') -- full lowercase hex in practice
ORDER BY l.block_number, l.log_index;
topic0 is the keccak-256 hash of Transfer(address,address,uint256). topic1 and topic2 hold the sender and recipient padded to 32 bytes, so the address is the last 40 hex characters, starting at position 27 after the "0x" and 24 padding zeros. The amount sits in data as hex; the function that converts it to a number differs between engines.
The results show three swaps through a DEX aggregator within 15 minutes, turning 1,912,400 USDC into 731.2 ETH, an average of about 2,615 USDC per ETH including price impact (see DEX swaps and liquidity flows). The 210 WETH is unwrapped to ETH. The exploiter account now holds 941.2 ETH of proceeds.
Step 3: Crossing chains
Between 10:31 and 11:05 UTC the funds split three ways (see Cross-chain tracing):
- 600 ETH goes in six deposits of 100 ETH to a cross-chain swap protocol. Each deposit carries a memo naming a Bitcoin destination, bc1q…7k3f. On Bitcoin, the protocol's six outbound payments to that address total 22.10 BTC, and each carries the matching inbound Ethereum hash in an OP_RETURN output. The link is explicit in the data on both sides.
- 300 ETH goes in three deposits of 100 ETH to a fast bridge whose deposit events do not name a recipient; a liquidity provider pays out on the rollup from its own funds. The only way to link the sides is by amount and time. Running the candidate-count query from the lesson Confidence, uncertainty and language returns exactly one release of 99.80 to 100.00 ETH within 20 minutes of each deposit, all three to 0x4B9e…07Fd, totaling 299.82 ETH.
- 41.2 ETH stays in the exploiter account, unmoved up to the cut-off.
Step 4: The Bitcoin peel chain and the exchanges
From bc1q…7k3f, the first transaction spends all six outputs and starts a peel chain: nine hops, each with one small payment out and one large remainder. The analyst follows it with a recursive query over inputs and outputs tables (see Querying UTXO data):
-- Follow the peel chain: at each hop keep the larger output as the change
WITH RECURSIVE chain (hop, txid, vout, value_sat) AS (
SELECT 1, transaction_hash, output_index, value FROM outputs
WHERE transaction_hash = 'e2d9…5a10' AND output_index = 1 -- change of hop 1
UNION ALL
SELECT c.hop + 1, o.transaction_hash, o.output_index, o.value
FROM chain c
JOIN inputs i ON i.spent_transaction_hash = c.txid
AND i.spent_output_index = c.vout
JOIN outputs o ON o.transaction_hash = i.transaction_hash
WHERE c.hop < 9 AND o.value = (SELECT MAX(value) FROM outputs
WHERE transaction_hash = i.transaction_hash)
)
SELECT hop, txid, value_sat / 1e8 AS value_btc FROM chain ORDER BY hop;
The recursive step joins each change output to the transaction that spends it and keeps that transaction's larger output, which is the remainder in a peel chain. Support for WITH RECURSIVE and its limits vary between engines. "Largest output" is only a rule of thumb, so the analyst checks every hop against the signals in the lesson Change detection heuristics: the change always goes to a fresh P2WPKH address matching the input type, the payments are round (0.75, 0.95, 1.00 BTC) while the change is not, and every transaction shares the same version, fee rate and locktime pattern, a consistent wallet fingerprint.
| Destination of the peels | Peels | Total |
|---|---|---|
| Exchange K deposit addresses | 3 | 2.95 BTC |
| Exchange O deposit addresses | 2 | 1.80 BTC |
| Unclustered addresses, unspent | 4 | 3.10 BTC |
| Remainder after hop 9, unspent | – | 14.248 BTC |
| Fees across nine hops | – | about 0.002 BTC |
The rows sum to the 22.10 BTC that entered. The exchange attributions come from what happened next: each deposit address was later spent together with more than 100 other addresses into a single hot wallet, the sweep pattern described in the lessons The common-input ownership heuristic and Following value through exchanges and services. Exchange K's hot wallet appears in a reserve address list that Exchange K itself published, a self-identified label; Exchange O's comes only from a third-party tag (see Attribution and labeling).
Step 5: Testing hypotheses and stating confidence
Who controls bc1q…7k3f? The analyst runs a small ACH matrix with three hypotheses: the exploiter (H1), an address of the swap protocol itself (H2), or a third party such as an over-the-counter desk paid by the exploiter (H3). The OP_RETURN references are consistent with all three: they prove the funds went there, not who controls the address. Diagnostic items are the memo set by the exploiter (inconsistent with H2), the address having no history before (inconsistent with a long-lived protocol address) and the immediate peel to small exchange deposits (inconsistent with a desk settling a trade into its own established wallets). H1 survives; H3 rests on a single behavioral argument, so the sensitivity check keeps it alive as a minority view.
The key judgments, using the ICD 203 scale declared in the report's method section:
- Fact: 1,912,400 USDC and 210 WETH left the Corvane market in transaction 0x9c4e…a71b, block 24,790,112.
- It is almost certain that 600 ETH of the proceeds became 22.10 BTC paid to bc1q…7k3f. Confidence is high: the link is recorded on both chains.
- It is very likely that 2.95 BTC reached Exchange K deposit addresses. Confidence is moderate: the label is self-identified, but the path relies on change detection across up to seven hops.
- It is likely that 1.80 BTC reached Exchange O deposit addresses. Confidence is low, because the label has a single third-party source.
- It is likely that the exploiter controls 0x4B9e…07Fd on the rollup. Confidence is moderate: three separate amount-time matches, each with one candidate.
The unmoved balances (41.2 ETH, 299.82 ETH, 3.10 BTC and 14.248 BTC) are reported as facts valid at the cut-off blocks.
Step 6: Report, visuals and disclosure
The report follows the lesson Writing the analytical report: a one-page summary with the five judgments, the scope, the method, one finding per judgment, alternatives and limitations, and an appendix of 35 transactions with full hashes, block heights and block hashes, generated by query. Following Visualizing flows and graphs, it has two Sankey diagrams, one in ETH and one in BTC rather than one mixing units, a flow graph with the bridge link dashed, and a timeline with one lane per chain showing that the first exchange deposit came less than five hours after the exploit.
Disclosure follows Data protection, ethics and responsible disclosure. Exchanges K and O are notified within the hour through their compliance channels, each receiving only its own deposit addresses, transaction hashes, amounts and the likelihood and confidence of the link. The rollup address is flagged to the bridge operator as a likely, not established, link. One unclustered peel address resolves through a naming service to what looks like a person's name: it stays out of every shared document and goes only into the restricted annex for legal process. The analyst also found two forks of Corvane's code with the same vulnerable price-oracle setup, still holding funds; both teams are told privately, and the public version of the report, which names no private individuals and no unclustered addresses, is released only after they confirm a fix.
Exam focus: turning "track the money" into a scoped analytic question; reading a receipt's status and decoded Transfer logs, filtering logs by topic0, extracting addresses from padded topics, and normalizing USDC (6 decimals) and WETH (18 decimals); why a memo plus OP_RETURN link is stronger than an amount-time match; following a peel chain recursively and confirming change with several heuristics; attributing deposit addresses through sweeps, and why a self-identified label beats a third-party tag; separate likelihood and confidence statements per judgment; reconciling every unit of value, including fees and unmoved balances; minimal, confidence-labeled disclosure, a restricted identity annex and private notice to vulnerable forks.
Practice questions
Check your understanding. These questions do not count towards the exam.
Sign in to save your progress and continue where you left off. It is free. Sign in