Back to Blog

~$5.98M Lost: Aztec, Raydium & More | BlockSec Weekly

Code Auditing
June 17, 2026
12 min read
Key Insights

During the past week (2026/06/08 - 2026/06/14), 4 notable incidents were detected across Ethereum and Solana, resulting in approximately $5.98M in total losses. The table below highlights the representative events:

Date Incident Type Estimated Loss
2026/06/08 Flooring Protocol Integer Overflow ~$900K
2026/06/09 Top Token Governance Attack ~$1.59M
2026/06/10 Raydium (on Solana) Lack of Input Validation ~$1.34M
2026/06/14 Aztec Lack of Input Validation ~$2.15M
  • Aztec: A validation gap between the rollup's proof path and L1 settlement path allowed the two to process different transaction sets, reaching inconsistent states.
  • Raydium: A missing validation check allowed the attacker to manipulate the LP token redemption calculation, draining full reserves from four pools.

Best Security Auditor for Web3

Validate design, code, and business logic before launch

Weekly Highlight: Aztec

In this incident, the ZK proof verifier and the L1 settlement logic processed different transaction sets because a single parameter was left unbounded. This consistency gap between proof and settlement applies to any rollup design where these two paths run as separate code.

On June 14, 2026, Aztec Connect, a privacy-focused rollup on Ethereum, was exploited for approximately $2.15M [1]. The root cause was a mismatch between the verified rollup transaction set and the L1 settlement processing boundary, which allowed the ZK proof path and the settlement logic to process different transaction lists. The attacker exploited this gap to credit unbacked deposit balances in the rollup state, then withdrew them through normal settlement flows.

Background

Aztec Connect is a privacy-focused rollup on Ethereum that enables private transactions on L2. Since user funds originate on L1, they must first be deposited into the rollup processor contract before they can be represented as notes in the L2 Merkle tree.

The deposit process works in two stages:

Stage 1: The user calls depositPendingFunds(), which increases userPendingDeposits[assetId][owner] through increasePendingDepositBalance() and transfers the tokens into the RollupProcessor. This creates a pending deposit on L1.

function depositPendingFunds(uint256 _assetId, uint256 _amount, address _owner, bytes32 _proofHash) external {
    increasePendingDepositBalance(_assetId, _owner, _amount);
    // ... transfer tokens into the contract
}

Stage 2: The user submits a deposit proof, which is later included in a rollup and added to the L2 state. When processRollup() runs, decodeProof() reads numTxs from the encoded calldata and returns it alongside the decoded proof data. Both are then passed to processRollupProof():

function processRollup(bytes calldata, bytes calldata _signatures) external {
    (bytes memory proofData, uint256 numTxs, uint256 publicInputsHash) = decodeProof();
    processRollupProof(proofData, _signatures, numTxs, publicInputsHash, rollupBeneficiary);
}

Inside processRollupProof(), two functions are called sequentially. First, verifyProofAndUpdateState() verifies the ZK proof against all decoded transactions and updates the rollup state. Then, processDepositsAndWithdrawals() handles L1 settlement, iterating only the first _numTxs slots and calling decreasePendingDepositBalance() for each deposit (this call reverts if the user did not actually deposit funds in Stage 1, tying the rollup credit to a real L1 transfer):

function processRollupProof(bytes memory _proofData, bytes memory _signatures,
    uint256 _numTxs, uint256 _publicInputsHash, address _rollupBeneficiary) internal {
    verifyProofAndUpdateState(_proofData, _publicInputsHash);       // proof path: all decoded transactions
    processDepositsAndWithdrawals(_proofData, _numTxs, _signatures); // settlement path: first _numTxs only
}
// inside processDepositsAndWithdrawals:
end := add(proofDataPtr, mul(_numTxs, TX_PUBLIC_INPUT_LENGTH))
while (proofDataPtr < end) {
    // ... for each deposit:
    decreasePendingDepositBalance(assetId, publicOwner, publicValue);
}

This two-stage design requires that the L1 settlement logic processes exactly the same set of transactions that the ZK proof verified. If the two paths mismatch on which transactions to process, deposits can be credited in the rollup state without consuming their pending balances on L1.

Vulnerability Analysis

In the rollup processor contract (0x7d65...2728), the circuit internally enforced the correct num_txs but did not expose it as a public input. The Solidity side read numTxs from unverified calldata metadata, so the proof path and the settlement path could process different transaction lists.

In the off-chain rollup_circuit, num_txs is loaded as a witness and used to gate which slots are treated as real transactions. The circuit does enforce that num_txs matches the actual count of non-padding proofs — if a slot contains a real proof (non-zero data root) but is_real is false (because i >= num_txs), the is_real.assert_equal(data_root_exists) check fails and proof generation aborts. However, num_txs is not exposed as a public input of the proof:

const auto num_txs = uint32_ct(witness_ct(&composer, rollup.num_txs));
field_ct(num_txs).create_range_constraint(MAX_TXS_BIT_LENGTH);
// ...
auto is_real = num_txs > uint32_ct(&composer, i);  // gates real-tx logic per slot
// ...
is_real.assert_equal(data_root_exists);  // enforces num_txs == actual real tx count
Circuit code showing num_txs enforced internally but not exposed as public input
Circuit code showing num_txs enforced internally but not exposed as public input

On the Solidity side, decodeProof() reads numTxs from calldata metadata that is not copied into the reconstructed proofData verified by verifyProofAndUpdateState(). Because the circuit's internal num_txs is not a public input, the verifier has no way to check whether the calldata numTxs matches the value proven inside the circuit:

decodeProof showing numTxs metadata not included in verified proofData
decodeProof showing numTxs metadata not included in verified proofData

An attacker could therefore set the calldata numTxs lower than the circuit's proven num_txs. The settlement loop would then skip transactions that the proof had credited in the rollup state. A non-actionable transaction could occupy the first decoded slot (within the settlement scan range), while a real deposit could sit in a later slot (proven by the circuit but outside the settlement scan range). The proof would credit the deposit in the rollup state, but the settlement logic would skip it entirely, including the decreasePendingDepositBalance() call. This left the pending deposit balance unconsumed on L1 while the rollup state already reflected the deposit.

Attack Analysis

The following analysis is based on the transaction 0x074ec9...9aeeb1.

The attacker exploited the gap between the proof path and the settlement path in two phases.

Phase 1: Creating unbacked balances

  • Step 1: The attacker submitted multiple rollup batches, each containing two decoded transactions: a non-actionable (junk) transaction in slot 1 and a real deposit in slot 2. The circuit's num_txs was set to 2 (matching both real transactions), but the calldata numTxs was set to 1. The L1 settlement logic processed only the junk transaction in slot 1, completely skipping the real deposit in slot 2.

  • Step 2: The ZK proof, however, verified and credited all decoded transactions including the deposit in slot 2. Because the settlement logic never reached this deposit, decreasePendingDepositBalance() was not called, and the L1 pending deposit balance remained unconsumed. The attacker repeated this pattern for seven different assets, building up unbacked balances in the rollup state.

Phase 2: Extracting funds

  • Step 3: Once the seven unbacked balances were established, the attacker initiated standard withdrawals for each asset. These withdrawals appeared legitimate to the settlement logic because the balances existed in the rollup state, so the L1 contract released the corresponding funds — approximately $2.15M in total.

Conclusion

This vulnerability was not a cryptographic weakness but a state consistency bug between two critical code paths in the rollup architecture. The root cause: while the circuit internally enforced that num_txs matched the actual real transaction count, this value was not exposed as a public input. The Solidity decoder read numTxs from unverified calldata metadata with no way to check it against the proven value. The attacker set the calldata numTxs lower than the circuit's proven count so that the settlement logic skipped deposits that the proof had already credited in the rollup state. The resulting unbacked balances were then withdrawn through normal settlement flows.

The vulnerability predated the Aztec Connect sunset. The rollup announced a sunset with transaction processing and withdrawals scheduled to end by March 31, 2024 [2]. However, the rollup processor contract was upgraded on April 10, 2024 via a pull request [3], which opened proof submission to all users — making the pre-existing vulnerability exploitable by anyone.

The fix requires binding numTxs to the full set of transactions verified by the ZK proof so that both paths always process the same set. Any rollup design that separates proof verification from L1 settlement must enforce that both paths operate on an identical, verifiably bounded transaction set. A discrepancy in even one parameter can turn an otherwise sound proof system into a vector for unbacked balance creation.

References

Get Started with Phalcon Explorer

Dive into Transactions to Act Wisely

Try now for free

More Incidents This Week

Raydium

On June 10, 2026, four pools on Raydium's legacy AMM v3 program on Solana were exploited for approximately $1.34M [1]. The withdrawal handler did not verify that a caller-supplied account matched the pool's stored counterpart, so the attacker substituted a controlled account to manipulate the payout calculation. The same technique drained all reserves from four pools within seconds.

Background

Raydium's AMM is a constant-product market maker on Solana. Each pool holds two token vaults and mints an LP token representing a proportional share of the reserves. When a liquidity provider withdraws, the handler computes the payout proportionally and transfers the corresponding share of both vaults:

coin_out = total_coin * withdraw_amount / lp_supply
pc_out   = total_pc   * withdraw_amount / lp_supply

On Solana, each token type is defined by a Mint account that stores the total supply, decimals, and mint authority. Each holder's balance is stored in a separate Token account bound to that Mint — one Mint can have many Token accounts across different holders. This differs from EVM, where a single ERC-20 contract manages both the token definition and all balances internally.

In the withdrawal formula above, lp_supply is read from the pool's LP Mint account — the one that tracks total LP supply. The correctness of the calculation depends on this value being the real LP Mint. However, on Solana, the caller passes every account into each instruction positionally, so the handler must validate that each caller-supplied account matches the canonical account stored in the pool state.

Vulnerability Analysis

The exploited program (27haf8...8vQv) was not open-sourced, and its executable data (ProgramData) was closed after the attack, making direct bytecode inspection impossible. The analysis below is based on the bytecode reconstructed from the program's last upgrade buffer and cross-referenced with on-chain transaction behavior.

In the withdrawal handler, the LP Mint account passed by the caller was not bound to the pool's recorded amm.lp_mint. The following reverse-engineered pseudo-code reconstructed from the on-chain bytecode shows the account layout. The handler checked bindings for the pool state, PDA authority, both vaults, and user accounts — but not for the LP Mint at slot 5:

let amm_info         = next_account_info(it)?;  // accounts[1] — pool state (holds amm.lp_mint)
// ...
let amm_lp_mint_info = next_account_info(it)?;  // accounts[5] — caller-supplied mint

let amm = AmmInfo::load(amm_info)?;
// authority, vaults, open_orders bindings checked here...
// >>> MISSING: check that accounts[5].key == amm.lp_mint <<<

let lp_mint = Mint::unpack(&amm_lp_mint_info.data.borrow())?;
let lp_mint_supply = lp_mint.supply;  // reads from the unverified mint

let coin_amount = total_coin * withdraw_amount / lp_mint_supply;
let pc_amount   = total_pc   * withdraw_amount / lp_mint_supply;

Because the LP Mint account was unbound, an attacker could substitute a Mint account they fully controlled. Setting its total supply to 1 and burning 1 token yielded a payout ratio of 1 / 1 = 100% of each reserve.

The vulnerable code had been live and unchanged since the program's last upgrade on January 3, 2023, approximately 1,254 days before the exploit.

Attack Analysis

The following analysis is based on the transaction 1csN6v...3s7s.

  • Step 1: The attacker created a fake LP Mint account with decimals = 0 and total supply = 0.
  • Step 2: The attacker initialized a Token account bound to the fake LP Mint, then minted exactly 1 token into it (as the Mint authority), pinning the Mint's total supply to 1.
  • Step 3: The attacker called the withdrawal function, passing the fake LP Mint in the expected account slot and the Token account from Step 2 (holding 1 fake LP token) as the LP source. With withdraw_amount = 1 and lp_supply = 1, the handler computed total_coin * 1 / 1 and total_pc * 1 / 1, which equaled 100% of both reserves (893,700 USDC and 66,837 RAY for the RAY/USDC pool).
  • Step 4: The handler burned the attacker's 1 token and transferred the full reserves out of both pool vaults, which drained the RAY/USDC pool entirely.

The attacker repeated the same pattern against three more pools within approximately 15 seconds. Across all four pools, the drained amounts were:

Pool Drained (approx.)
RAY/USDC ~66,837 RAY + ~893,700 USDC
RAY/wSOL ~74,720 RAY + ~5,603 wSOL
RAY/SRM ~8,622 RAY + ~10,692 SRM
RAY/Sollet ETH ~5,038 RAY + ~16 Sollet ETH

Conclusion

The root cause is a single missing account-validation check: the withdrawal handler used the supply of a caller-supplied Mint account as the LP supply divisor without binding it to the pool's recorded amm.lp_mint. On Solana, every caller-supplied account must be bound to its canonical counterpart stored in the pool state. A correct implementation should reject any LP Mint whose key does not match the pool's stored record, and compute redemption from a pool-internal LP counter rather than the externally supplied Mint's supply. The exploited contract was an older deployment (last upgraded January 2023) that was closed the same day as the attack. According to the Raydium team, full compensation will be handled by Raydium's treasury [1].

References

Get Started with Phalcon Security

Detect every threat, alert what matters, and block attacks.

Try now for free

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.

Sign up for the latest updates
Newsletter - June 2026
Security Insights

Newsletter - June 2026

This monthly report covers the three largest security incidents in June 2026, totaling approximately $22M in confirmed losses. A sophisticated honeypot attack drained ~$15M from JaredFromSubway's MEV bot by exploiting unchecked token allowances. Two legacy Aztec rollup deployments lost ~$4.35M through proof-settlement boundary gaps. SecondFi's Ed25519 implementation flaw exposed wallet private keys, resulting in ~$2.4M drained from 374 wallets. All three incidents share a common pattern: security guarantees that appeared intact on the surface but were never actually enforced.

~$4.1M Lost: Taiko, SecondFi Exploits | BlockSec Weekly
Security Insights

~$4.1M Lost: Taiko, SecondFi Exploits | BlockSec Weekly

This weekly blockchain security report covers two notable incidents from June 22-28, 2026, with approximately $4.1M in confirmed losses across Ethereum and Cardano. The Taiko bridge exploit combined an exposed SGX enclave signing key with an incomplete attestation policy that failed to reject debug enclaves, allowing the attacker to register a malicious prover and forge L2 state proofs on Ethereum. The SecondFi wallet vulnerability stemmed from a cryptographic implementation flaw in Ed25519 nonce derivation that removed the secret input, enabling offline private key recovery from public Cardano transaction data.

~$18M Lost: jaredFromSubway, Aztec & More | BlockSec Weekly
Security Insights

~$18M Lost: jaredFromSubway, Aztec & More | BlockSec Weekly

This weekly blockchain security report covers June 15 to June 21, 2026, with 3 notable incidents across Ethereum and BNB Chain totaling approximately $18.3M in losses. Two incidents are analyzed in detail. Based on on-chain analysis, the highlighted jaredFromSubway incident reveals a reversed approval attack pattern: unlike traditional exploits where attackers abuse vulnerabilities in trusted DeFi contracts to drain user-approved assets, this MEV bot proactively approved its own assets to untrusted third-party contracts for arbitrage. The attacker constructed fake wrapper tokens and swap pools that emitted real events but never consumed the granted allowances, with reported total losses of ~$15M. The report also covers Aztec's second exploit in three days, where a missing equality constraint between two witnesses for `old_data_root` in the escape hatch ZK circuit allowed the attacker to prove ownership of fabricated notes against a fake Merkle tree while passing on-chain root validation.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit