During the past week (2026/06/15 - 2026/06/21), we observed 3 notable security incidents with total losses of approximately $18.3M.
| Date | Incident | Type | Estimated Loss |
|---|---|---|---|
| 2026/06/18 | Aztec | Improper Public Input Binding | ~$2.2M |
| 2026/06/20 | LABUBU Token | Misconfiguration | ~$1.1M |
| 2026/06/20 | jaredFromSubway | Improper Approval Management | ~$15M |
- Aztec: Selected because the protocol was exploited a second time within three days, this time through its escape hatch circuit, highlighting recurring ZK proof binding issues.
- jaredFromSubway: Selected because the MEV bot's contract granted approvals to untrusted token contracts without verifying consumption or revoking residual allowances, allowing the attacker to accumulate and drain ~$15M.
Best Security Auditor for Web3
Validate design, code, and business logic before launch
Weekly Highlight: jaredFromSubway
Unlike traditional approval exploits — where attackers abuse vulnerabilities in trusted DeFi contracts to drain assets that users approved to those contracts — this attack targets the reverse direction: the MEV bot proactively granted approvals on its own assets to untrusted third-party contracts as part of its arbitrage operations. The attacker constructed a fake trading environment (essentially a honeypot) where fake swap pools emitted real Swap and Sync events while the fake tokens never consumed the granted allowances, accumulating these outward-facing approvals before harvesting them, with reported total losses of ~$15M.
On June 20, 2026, jaredFromSubway, an MEV bot operator on Ethereum, lost approximately $15M [1]. Based on on-chain analysis, the root cause was improper approval management in the bot contract: approvals were granted to untrusted wrapper contracts that never consumed them, and the attacker accumulated these unconsumed allowances until draining the bot's real balances in a single transaction.
Background
jaredFromSubway is a well-known MEV bot operator on Ethereum, specializing in sandwich attacks and on-chain arbitrage. The victim contract (0x1f2f...f387) is one of its operating wallets, holding large working-capital balances in WETH, USDC, and USDT.
MEV bots of this kind must dynamically interact with arbitrary new tokens and pools that appear on-chain. They monitor the mempool, simulate transactions, and automatically approve token interactions to capture arbitrage opportunities. This operational model relies on the assumption that tokens behave as expected: when a swap is executed, the token contract consumes the granted allowance by calling transferFrom.
Vulnerability Analysis
The root cause is the MEV bot's improper approval management when interacting with untrusted contracts.
The bot executes various arbitrage paths across Uniswap pools and routers. In most interactions, the bot pushes tokens to pools directly via transfer, where the bot itself is msg.sender and no approval is needed. However, interactions with wrapper-style token contracts follow a pull model: the bot calls wrapper.wrapTo(), and inside that call the wrapper contract calls realToken.transferFrom(bot, wrapper, amount) to pull the bot's real tokens. Since msg.sender during transferFrom is the wrapper contract — not the bot — a prior approve is required:
<real_token>.approve(tokenContract, amount)— grant allowance on the real token to the wrapper contracttokenContract.wrapTo()→ multi-hopswap()across pools →tokenContract.unwrap()— wrap the real token, route through pools, and unwrap back to the real token
The bot assumed that wrapTo() would consume the allowance via transferFrom, as a well-behaved wrapper contract would. However, the bot never verified whether the allowance was actually consumed after the operation, nor revoked any residual allowance. If wrapTo() does not call transferFrom, the full allowance survives the operation and becomes a persistent attack surface — any contract holding such an allowance can later call transferFrom to move the bot's real assets.
Attack Analysis
Based on on-chain reconstruction, the attacker constructed a fake trading environment with three components to exploit the vulnerability described above:
-
Fake wrapper tokens: Each fake token used the real token's name but prefixed the symbol with
f(e.g., nameUSD Coinwith symbolfUSDCfor USDC). It implementedwrapTo()andunwrap()to mimic a legitimate wrapper, plus an attacker-restrictedwithdraw()function that drained unconsumed allowances viatransferFrom. -
Fake swap pools: The attacker deployed approximately 44 Uniswap V2-style pools via a self-deployed factory. These pools paired fake tokens with each other to form convincing swap routes. When
swap()was called, the pools emitted realSyncandSwapevents indistinguishable from legitimate trades. -
Attacker-crafted profits: During
unwrap(), the fake token sent a small amount of real tokens back to the bot viatransfer. The bot received real profit, but it was deliberately crafted by the attacker rather than earned from market arbitrage.
The attacker controlled these components via a per-block getStatus() switch in an external contract. getStatus() returned 1 when called in the same block as an activation transaction (which set _getStatus = block.number), and 0 otherwise. When getStatus() == 0, wrapTo() called transferFrom normally and the allowance was consumed. When getStatus() == 1, wrapTo() skipped transferFrom — the allowance was not consumed — while unwrap() still returned attacker-crafted tokens to the bot. The attacker likely used builder bribes to place the activation transaction in the same block as the bot's transaction when they wanted to accumulate allowances.
The attack proceeded in three phases:
Phase 1: Deploy attack infrastructure
-
Step 1: The attacker set up the infrastructure across blocks 25354424 to 25354519. This included deploying a fake token factory contract (0x81f2...0091), creating ~44 fake Uniswap V2 pools via a self-deployed factory, funding the pools with initial token balances so
swap()calls would succeed, and sending 0.01 ETH to the harvest contract (0xb84d...df52) for gas and builder bribe. -
Step 2: The attacker mass-produced fake wrapper tokens via CREATE2, each mimicking a real token (using the real name but prefixing the symbol with
f) and carrying an attacker-restrictedwithdraw()function. CREATE2 gave deterministic addresses that the harvest contract could loop over.
Phase 2: Build trust and accumulate approvals
-
Step 3 (initial trust): In the earliest transactions (e.g., 0x542d...362b at block 25354425), the fake tokens had no
getStatus()switch —wrapTo()calledtransferFromdirectly, consuming the allowance. The bot approved, wrapped, swapped, unwrapped, and profited normally. This established the fake tokens as profitable trading opportunities. -
Step 4 (continued trust): In subsequent transactions (e.g., 0x085e...37e51), the
getStatus()switch was deployed but returned 0 (different block from activation).wrapTo()still calledtransferFromand consumed the allowance. The bot continued to profit and kept interacting. -
Step 5 (accumulation): Starting from 0x8560...1915 at block 25360519, the attacker placed an activation transaction in the same block as the bot's transaction via builder bribe, causing
getStatus()to return 1. In this mode,wrapTo()skippedtransferFrom— the allowance was not consumed — butunwrap()still sent a small amount of real tokens back to the bot. The bot saw a profitable operation and left the approval in place. Over approximately 600 blocks (~13 transactions), the bot repeated this pattern acrossWETH,USDC, andUSDT, accumulating unconsumed allowances on all three real assets.
Phase 3: Harvest
- Step 6: The attacker called
withdraw()on all fake tokens in the harvest transaction 0x2be870...cf3e65, using the unconsumed allowances to calltransferFromand move the bot's real balances to the attacker. A 0.01 ETH builder bribe was included to ensure block inclusion. The harvest extracted 1,474.58WETH+ 2,870,573USDC+ 2,035,760USDT(~$7.5M) from the victim contract alone.
The identified attack transaction leads to ~$7.5M in losses, and the total loss is approximately $15M based on jaredFromSubway's claim [1].
Conclusion
The root cause of this incident was the MEV bot's improper approval management when interacting with untrusted contracts. Unlike traditional approval exploits where attackers abuse vulnerabilities in trusted DeFi contracts to drain user-approved assets, this attack works in the reverse direction: the bot proactively approved its own assets to untrusted third-party contracts as part of its arbitrage operations. The attacker accumulated these outward-facing, unconsumed approvals and harvested them in a single transaction.
To reduce similar risks in the future, bot contracts interacting with untrusted token contracts should verify that approvals are consumed after each operation and revoke any residual allowance. Unconsumed allowances after a successful-looking trade are a strong signal of malicious token behavior.
More Incidents This Week
Aztec
On June 18, 2026, a missing equality constraint in Aztec's escape hatch ZK circuit allowed an attacker to withdraw approximately $2.2M (1,158 ETH, 150K DAI, and ~0.47 renBTC) from the legacy RollupProcessor contract on Ethereum [2], [3]. This was the second Aztec exploit in three days (the first, covered in our previous report, targeted the upgraded RollupProcessorV3) through a related ZK circuit public input binding bug.
Background
Aztec's legacy RollupProcessor includes an escapeHatch function: a safety mechanism that lets anyone submit a single-transaction proof when the rollup operator stops processing. Unlike processRollup (which requires an authorized provider), the escape hatch opens in periodic windows based on block number and is callable by anyone:
function escapeHatch(
bytes calldata proofData,
bytes calldata signatures,
bytes calldata viewingKeys
) external override whenNotPaused {
(bool isOpen, ) = getEscapeHatchStatus();
require(isOpen, 'Rollup Processor: ESCAPE_BLOCK_RANGE_INCORRECT');
processRollupProof(proofData, signatures, viewingKeys);
}
The escape hatch uses a dedicated ZK circuit (escape_hatch_circuit) that processes a join-split transaction: consuming input notes from the Merkle tree and creating output notes. The circuit must verify that the input notes exist in the current data tree (using old_data_root for Merkle membership), then expose the same root as a public input for the L1 contract to validate against on-chain state.
Vulnerability Analysis
The vulnerability is in the escape hatch circuit (escape_hatch_circuit.cpp). The old_data_root value is turned into two independent witnesses with no equality constraint connecting them.
The first witness (line 33) is passed into the join-split circuit component, where it is used for Merkle membership proofs to verify that input notes exist in the data tree:
join_split_inputs inputs = {
// ...
witness_ct(&composer, tx.js_tx.old_data_root), // line 33: first witness
// ...
};
auto outputs = join_split_circuit_component(composer, inputs);
The second witness (line 50) is created independently and exposed as a public input (line 88), which the Solidity contract extracts and checks against the on-chain data root:
auto old_data_root = field_ct(witness_ct(&composer, tx.js_tx.old_data_root)); // line 50: second witness
// ...
composer.set_public_input(old_data_root.witness_index); // line 88: exposed as public input
In a ZK circuit, each witness_ct call creates an independent variable. Without an explicit assert_equal between lines 33 and 50, the prover can assign different values to these two witnesses. On the Solidity side, validateMerkleRoots checks require(oldDataRoot == dataRoot) using only the public input from line 50, with no visibility into the value used at line 33.
The same unbinding pattern also exists for
input_ownerandoutput_owner: these values are witnessed at lines 38–39 (passed intojoin_split_circuit_componentfor ownership verification) and again at lines 111–112 (exposed as independent public witnesses). However, we have not identified a practical exploit path for this gap.
Attack Analysis
The escape hatch circuit was removed from the aztec-connect codebase [4], but the deployed verifier contract still contains the EscapeHatchVk verification key, so proofs generated with the vulnerable circuit can still pass on-chain verification. At the time of the attack, the contract had been silent for 142 days and the escape hatch window was open. The attacker's address had been created just 14 hours before the exploit via Union Chain [2]. The attack consists of three core steps:
-
Step 1: The attacker constructed a fake Merkle tree containing self-owned notes of arbitrary value. These notes did not exist in the real on-chain data tree (the Merkle tree storing all valid notes).
-
Step 2: The attacker generated an escape hatch proof exploiting the unbound witnesses described above. Line 33's witness (used for Merkle membership in the join-split component) was set to the fake Merkle root (the membership check passed because the fabricated notes existed in the fake tree, and ownership checks passed because the attacker held the signing keys). Line 50's witness (exposed as the public input checked by Solidity) was set to the real on-chain data root (the Solidity
require(oldDataRoot == dataRoot)check passed because this value matched the contract's stored root). -
Step 3: With both the circuit and Solidity checks satisfied, the proof verified successfully. The contract processed the escape hatch transaction as legitimate and released the funds.
The attacker repeated this process across three transactions (0x9e1d6a...6b03ca, 0xab306c...59c2b5, 0x5c196c...4705c3), targeting different assets and extracting 1,158 ETH, 150K DAI, and ~0.47 renBTC respectively, totaling approximately $2.2M.
Conclusion
The root cause of this incident was a missing equality constraint between two witnesses for old_data_root in the escape hatch circuit. One witness was used for private note membership verification inside the join-split component, the other was exposed as the public input checked by Solidity. Without a constraint binding them, the attacker proved ownership of fabricated notes against a fake Merkle tree while the L1 contract saw a valid on-chain root. Notably, removing the vulnerable circuit from source did not neutralize the already-deployed verifier contract — the escapeHatch function on the legacy RollupProcessor remains callable whenever its block-number window is open.
To reduce similar risks in the future, when the same logical value appears at multiple points in a ZK circuit, all instances must be explicitly constrained to be equal — independent witness_ct calls for the same value are a binding gap. Circuit audits should systematically verify that every public input is bound to the circuit-internal value it represents.
References
- [1] jaredFromSubway Official Statement (~$15M loss)
- [2] Phalcon Alert: Aztec Escape Hatch Exploit
- [3] Aztec Labs Official Statement
- [4] Escape Hatch Circuit Removal, aztec-connect commit 8c3953a
About BlockSec
BlockSec is a full-stack blockchain security and crypto compliance provider. We build products and services that help customers to perform code audit (including smart contracts, blockchain and wallets), intercept attacks in real time, analyze incidents, trace illicit funds, and meet AML/CFT obligations, across the full lifecycle of protocols and platforms.
BlockSec has published multiple blockchain security papers in prestigious conferences, reported several zero-day attacks of DeFi applications, blocked multiple hacks to rescue more than 20 million dollars, and secured billions of cryptocurrencies.
-
Official website: https://blocksec.com/
-
Official Twitter account: https://twitter.com/BlockSecTeam



