June 10, 2026. Five liquidity pools nobody had touched since 2021, and $1.34 million walked out of them in one transaction. The technical post-mortems — GoPlus Security's vulnerability breakdown, PeckShield's alert thread — did their job well and ended exactly where these reports always end: funds were bridged to Ethereum and deposited into Tornado Cash.
That's where the public record stopped. I kept going.
Short version: An attacker exploited a validation flaw in Raydium's retired AMM V3 program — dead since 2021, still live on-chain — using a counterfeit LP token to drain five legacy pools of RAY, SOL, and USDC. No admin keys, no governance compromise, just old code that never got turned off. The stolen assets were bridged from Solana to Ethereum, swapped to ETH, and deposited into Tornado Cash. From there, a Dune query against Tornado's withdrawal data surfaced eight withdrawals matching the deposit — two batches, 500 ETH and 300 ETH, both routed through Maya Protocol and swapped into ZEC on two transparent Zcash addresses. Both addresses shielded their balances shortly after. Both later deshielded again. Where the money went from there is the open question this piece ends on.
What Actually Happened: The Exploit in One Breath
Raydium's AMM V3 program was phased out in 2021, after the collapse of the Serum on-chain order book that it depended on. The contracts were never migrated to the current front end and hadn't processed a user-facing transaction in years. They were also never shut down.
According to GoPlus Security's post-incident analysis, the attacker didn't need a compromised key or a governance exploit — just a gap in how the old liquidity-removal logic validated LP tokens. They minted a custom SPL token designed to pass as a legitimate LP token for one of the dormant pools, and used it to bypass the proportional-withdrawal checks that are supposed to cap how much a given LP token can pull out. The contract trusted the forged receipt and paid out against it.
Five pools, six legacy trading pairs (RAY, SOL, USDC, USDT, ETH, SRM), one attacker. The haul: roughly 150,177 RAY, 5,603 SOL, and 893,700 USDC — about $1.34M at the time. The withdrawal itself is on-chain: 2gwZ1P37p3S3963YwovvzE1FF7zXY3g1Dj8BUVwzsUAQaJD2W7sxXx9geFcxaUny4tAEguzv187ha1YmE9iBZTMN.
Raydium confirmed current mainnet infrastructure uses stricter validation and was never exposed. No active user, and no pool reachable through the live interface, was affected. The team pledged a full treasury-funded reimbursement for anyone still holding a position in the dead pools — the same playbook it used after a 2022 admin-key compromise cost it roughly $2M.
Getting to the Mixer
Once the assets reached Ethereum, the attacker swapped everything to ETH. From there it followed the pattern you'd expect from someone who's done this before: straight into Tornado Cash, no delay, no side trips.
This is the point where on-chain tracing is supposed to break down. Tornado Cash severs the on-chain link between a specific deposit and a specific withdrawal cryptographically, via zk-SNARKs — that's not a reporting gap, it's the protocol doing exactly what it's built to do. What it doesn't hide is behavior around the deposit: batch size, timing, denomination, and what a withdrawal wallet does immediately after it receives funds. That's the entire premise of tracing past a mixer — the cryptography is airtight, the operational security around it usually isn't.
One relevant piece of context: OFAC removed Tornado Cash from its sanctions list in March 2025. Using the mixer today doesn't carry the same direct sanctions exposure it did in 2022–2024, which likely factors into why attackers are still routing straight through it in 2026 rather than avoiding it outright.
Querying the Withdrawals
I ran a query against Dune's Tornado Cash spellbook, restricted to the 100 ETH denomination pool and a five-day window starting right after the deposit:
SELECT block_time, tx_hash, recipient, tx_from, relayer, amount FROM tornado_cash.withdrawals WHERE blockchain = 'ethereum' AND amount = 100 AND block_time >= TIMESTAMP '2026-06-10 00:02' AND block_time <= TIMESTAMP '2026-06-15 00:01' ORDER BY block_time ASC
That returned 84 withdrawals across all assets using the 100-denomination pool in that window — too broad to mean much on its own. Restricting it to ETH specifically cut that number down considerably. I was looking for a total that added up to roughly 800 ETH, matching what went in.
Eight withdrawals fit. They split cleanly into two batches — 500 ETH and 300 ETH — and both batches were immediately routed through Maya Protocol, a cross-chain swap layer that lets funds move (and change form) across chains in a single step. That's the obfuscation move: instead of bridging or swapping in a way that leaves a conventional trail, both batches went straight from ETH into Maya's routing infrastructure.
The 500 ETH batch: A collector address (0x4ae61f0a...5e8f8056a) fed a hub address (0x20ef289f...82527152) in two transfers — 497.74128267 ETH and 0.1 ETH. That hub address then deposited into Maya Protocol across four separate transactions: 140.01 ETH, 140.01785369 ETH, 93.34523851 ETH, and 124.46032066 ETH.
The 300 ETH batch: A second collector address (0xb8ef2efd...7FEC5DdE3) received a couple of dust-value transfers — effectively zero-value, the kind of low-value pings that often precede a real transaction — before sending 298.70476692 ETH onward to 0xD2986fc7...467AE57d9, which fed into Maya Protocol the same way.
Where It Came Out
Both batches were swapped through Maya Protocol into ZEC. The receiving addresses on the Zcash side:
500 ETH batch → t1NFGfN5VXtxPP7b1z4fbT8EFtb8eczQd6N
300 ETH batch → t1WcLXfdFViZ8DF5ALFGSJwgvbuGjXeE13w
Both are transparent (t1...) Zcash addresses — visible, auditable, no different from a Bitcoin or Ethereum address in terms of what you can see on-chain. That visibility didn't last. Both addresses shielded their balances shortly after, moving the funds into Zcash's Sapling and Orchard pools.
This is the second real trail-break in this investigation, and it's a harder one than Tornado Cash. Shielded transactions aren't obscured by mixing or relayers — the input-to-output link is broken cryptographically, via zero-knowledge proofs. There is no on-chain data, for anyone, that proves which shielded output corresponds to which shielded input. Unlike Tornado Cash, there isn't even a batching or timing pattern to lean on once the pool has enough other traffic running through it — and Zcash's Orchard pool, at the time I was looking, had well over 100,000 ZEC in circulation through it, from hundreds of other depositors. At that scale, matching a specific input to a specific output isn't just difficult. It's statistically meaningless to claim.
What I can confirm: the funds didn't stay shielded forever. Both t1WcLXfdFViZ8DF5ALFGSJwgvbuGjXeE13w and t1NFGfN5VXtxPP7b1z4fbT8EFtb8eczQd6N show activity consistent with deshielding — value coming back out of Sapling/Orchard into transparent addresses downstream. That's a meaningful signal on its own: whoever moved this money didn't intend to leave it parked in the shielded pool. It went in, and it came back out, transparent again, somewhere else.
What's Still Open
The shielded hop is a real wall, not a soft one — I'm not going to claim a specific downstream address received "the" Raydium funds when the pool composition makes that unprovable. But the deshielding event itself reopens the trail. Whatever came out the other side is transparent again, and transparent means traceable the normal way: destination addresses, timing against the shielding event, exchange deposit patterns, the same toolkit that worked on the Ethereum side of this.
That's the next piece of work — not "the funds vanished into Zcash," but "the funds re-emerged from Zcash, and here's where."