
Date: July 2 to 3, 2026 (UTC)
Chain: Ethereum
Loss: about $800K, almost all of it USDC
Class: one deposit spent many times through notes that weren't bound to a single nullifier
At 22:11:35 UTC on July 2, 2026, the first of 85 withdrawals left Hinkal's shielded pool on Ethereum. Over the next two hours, seven minutes and 48 seconds, according to AML Crypto's tracing, the pool paid out 797,000 USDC and 0.08 ETH to an address beginning 0xbB3f01a1, and within a few hours most of it had been swapped to ETH, pushed through Tornado Cash and bridged to Bitcoin.
The attacker had deposited 100 USDC to start, at 20:08:47 UTC on July 2, and Hinkal's own update dates the start of the incident to 19:05 UTC that day. Hinkal's prooflessDeposit() function accepted a note in an old format, and BlockSec's analysis concludes that the old format likely let one note produce many different nullifiers, the one-time markers a privacy pool uses to stop the same deposit from being spent twice.
Hinkal is a privacy layer for Ethereum. Users deposit tokens into a shielded pool, where each balance is stored as a note: a commitment sitting in a Merkle tree that reveals nothing about who owns it or how much it holds. To spend a note, the owner generates a zero-knowledge proof off-chain showing that they own a valid note in the tree, and they publish that note's nullifier alongside it. The contract checks the proof, checks that the nullifier hasn't been seen before, records it, and pays out.
That design splits the work between two places. The contract can see nullifiers but can't see which note a nullifier came from, because hiding that link is the whole point of the pool. BlockSec puts it plainly: the rule that one note produces exactly one nullifier is delegated to the circuit, the program that defines what a proof is allowed to prove. If the circuit lets one note yield a second valid nullifier, the contract has no way to notice.
Hinkal had moved to a newer note format but still supported the old one. According to BlockSec, the newer format binds the owner's secret nullifying key directly into the note's hash, while the legacy format only mixes it in as a product with a random value. BlockSec's suspected defect is that a different pair of values can produce the same note while changing the key, which yields a fresh nullifier for the same deposit each time. Hinkal hasn't open-sourced its circuit, so BlockSec labels this an inference from on-chain behavior and de-obfuscated client code.
The contract side made the legacy path reachable. prooflessDeposit() lets users put funds into the pool without generating a proof first. BlockSec reports that it doesn't appear to reject legacy-format notes and doesn't use the return value of its own validation step. A flag in the top bit of a field called extraRandomization marks a note as new-style, Hinkal's client sets it by default, and the attacker's deposit had it set to zero.
The teaching moment for builders is that a pool stays solvent only if every note can be spent exactly once, and that rule has to hold on every path that creates a note. Think of a coat check that tears a numbered stub for each coat: if one desk prints stubs that can be reprinted with new numbers, every reprint gets a coat, and the attendant comparing stub numbers never sees anything wrong.
BlockSec reconstructs the attack in four steps. The attacker deposited 100 USDC through prooflessDeposit() in transaction 0xfbed...2f11, sent from 0x105a...D7C0 in block 25447119, with the new-style flag set to zero. They then called transact() over and over against that one note, each time with the same Merkle root and a new nullifier, withdrawing again and again until they'd collected about 25,000 USDC. On-chain, the attacker's first USDC withdrawals from the pool came in 1,000 USDC amounts.
Next, the attacker consolidated those 25,000 USDC into a single legacy note with another prooflessDeposit() call, transaction 0xbf72...0008, sent from 0xbB3f...fc20 at 00:07:23 UTC on July 3 in block 25448306, and repeated the loop against the bigger note. BlockSec notes that after that deposit the attacker never supplied more assets, yet withdrew far more than they had put in. Between blocks 25448345 and 25448347, The Crypto Times counts at least 14 outbound transfers of exactly 25,000 USDC, all within about a minute of chain time. AML Crypto's table shows 77 USDC withdrawals and eight ETH withdrawals of 0.01 ETH, and one 25,000 USDC transfer back into Hinkal at 00:07:23 UTC on July 3, which it reads as a failed further attempt and nets out to 772,000 USDC. That transfer is the consolidation deposit BlockSec describes, and Hinkal's own update puts the amount withdrawn at about 797K USDC.
At 00:17:11 UTC on July 3, the attacker began swapping USDC to ETH on Uniswap. The Crypto Times counts at least 410 ETH, about $700,000, deposited into Tornado Cash in 11 deposits of 10 ETH and three of 100 ETH between blocks 25448486 and 25448510, and PeckShield's alert, which put the loss at about $820K, reports 44.7 ETH bridged to Bitcoin through THORChain, landing at a Bitcoin address beginning bc1qr2sf. Blockscout shows that THORChain deposit going out at 01:23:23 UTC on July 3, in block 25448683, for 44.67 ETH.
Hinkal paused its contracts on every chain and, per BlockSec, committed to reimbursing affected users in full. The incident was limited to one contract and its pools on Ethereum. Hinkal also sent the attacker an on-chain message offering to treat the incident as a recovery if 90% of the funds came back to its owner address by July 11, 2026, with the attacker keeping 10%. AML Crypto reported no returned funds at publication, and as of October 19, 2026, Hinkal's X account hadn't reported any funds coming back from the attacker. Hinkal redeployed its EVM contracts on July 16 after what it described as an independent audit, and its July 20 update said it would begin returning funds to affected users that week, with all returns expected by July 22.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it uses the context around the code, including docs, design notes and changes like a format migration, to infer the invariants a system depends on but never writes down. Hinkal's circuit isn't public and runs off-chain, so it sits outside what the engine reads. The deposit path that let a legacy note into the pool is on-chain contract logic, and the rule it had to keep fits in a sentence: prooflessDeposit() should only create a note that passes its own validation, so that no deposit can bring a note into the pool that the rest of the system wasn't built to handle.
A contract's state space looks infinite, but it isn't, and the engine maps the paths through it and uses signals to decide which ones to attack. The signal in Hinkal is a deposit entry point that skips proof verification, accepts a note whose format flag selects a legacy code path, and calls a validation step whose result it never uses. A return value that's computed and then dropped on a path that moves funds is exactly the kind of pattern that narrows the search from every path, to the feasible ones, to the ones that can touch the invariant.
The engine attacks that path from two directions. Counterexample-Guided Invariant Refutation works outward from the system, taking the deposit rule as given and executing deposit sequences until one breaks it, while Bounded Adversarial Verification works inward from the signal, running known strategies against the ignored validation result. Symbolic execution underpins both. The counterexample here is short: a prooflessDeposit() call with the new-style flag set to zero, a validation step that fails, and a deposit that goes through anyway and leaves a legacy note in the tree. Olympix returns that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, which is the opening move of the July 2 attack, surfaced in development instead of on mainnet.
Olympix runs automatically in development, at every merge and before release, which matters most for code like this, where a legacy path stays live after a newer one ships and the two meet at a single entry point.
Hinkal's client created new-format notes by default, and the loss came through the older format that one door into the pool still accepted. Any protocol that migrates a format while keeping the old one alive for compatibility ends up with two sets of rules guarding one balance, and the invariant has to hold for both. To see how Olympix tests your own entry points against the invariants your system actually relies on, book a technical walkthrough.
Protocol: Hinkal Protocol
Chain: Ethereum
Date: July 2, 2026, 22:11:35 UTC to July 3, 2026, 00:19:23 UTC (withdrawal window, per AML Crypto)
Loss: about $800K per BlockSec and CertiK (797,000 USDC and 0.08 ETH withdrawn, 772,000 USDC net, per AML Crypto; PeckShield reported about $820K)
Attacker address: 0xbB3f...fc20
Hinkal pool contract: 0x25e5...a826
First depositor address: 0x105a...D7C0
Legacy deposit tx (100 USDC, 20:08:47 UTC, block 25447119): 0xfbed...2f11
Consolidation deposit tx (25,000 USDC, 00:07:23 UTC, block 25448306): 0xbf72...0008
Exploit tx, first withdrawal (0.01 ETH to the attacker address, July 2, 22:11:35 UTC, block 25447729): 0x9f33...ea5e
Exploit tx, first 25,000 USDC withdrawal (July 3, 00:15:11 UTC, block 25448345): 0x93ae...edc9
Hinkal owner address (recovery offer): 0xBdc7...1942
THORChain deposit tx (44.67 ETH): 0x14dd...6deb
THORChain destination (Bitcoin): bc1qr2...zn3w
BlockSec: About $800K Lost in Hinkal Double-Spend
AML Crypto: Hinkal Hack, How 772,000 USDC Was Stolen and Laundered
The Crypto Times: Hinkal Protocol Exploited, $820K Laundered via Tornado Cash and THORChain