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/15), 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/15 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 15, 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), numTxs was not effectively bound to the transaction set enforced by the ZK proof. The proof path and the settlement path could therefore process different transaction lists.

In the off-chain rollup_circuit, num_txs is loaded as a witness and only range-constrained. The circuit uses it to gate which slots are treated as real transactions, but does not verify that num_txs equals the actual count of non-padding proofs:

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

The prover can set num_txs to any value within the allowed range. Slots beyond num_txs are still recursively verified but their public inputs are zeroed, so they do not contribute to the rollup state:

On the Solidity side, decodeProof() reads numTxs from calldata metadata that is not copied into the reconstructed proofData verified by verifyProofAndUpdateState(). The settlement loop's boundary is therefore not covered by the ZK proof either:

With neither side constraining this value, an attacker could set numTxs lower than the actual number of decoded transactions. The settlement loop would then skip transactions that the proof still 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, with numTxs 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: numTxs was not bound to the proven transaction set on either side. The circuit only range-constrained it, and the Solidity decoder read it from unverified calldata metadata. Without this binding, the proof path and the settlement path could process different transaction lists. The attacker set numTxs lower than the actual transaction 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 Aztec Connect rollup announced a sunset, with transaction processing and withdrawals scheduled to end by March 31, 2024 [2]. However, the rollup processor contract was still upgraded on April 10, 2024 via a pull request [3], and the vulnerable logic is present in that post-sunset upgrade.

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
Zcash Orchard Soundness Bug Analysis | BlockSec Weekly
Security Insights

Zcash Orchard Soundness Bug Analysis | BlockSec Weekly

During the week of June 1, 2026, a critical soundness vulnerability was publicly disclosed in Zcash's Orchard shielded pool circuit, caused by a missing equality constraint in the halo2 ECC scalar multiplication gadget that could have enabled undetectable counterfeiting of ZEC within the Orchard pool through double-spending. The vulnerability, which existed for over four years since Orchard's activation in May 2022, was discovered by an AI-assisted security audit and patched through an emergency network upgrade (NU6.2). This single-event report covers the technical root cause (under-constrained ZK circuit relation), the AI-assisted discovery by researcher Taylor Hornby using Anthropic's Opus 4.8 model, the emergency response timeline, and the broader implications for the ZKP ecosystem.

Newsletter - May 2026
Security Insights

Newsletter - May 2026

In May 2026, the DeFi ecosystem experienced three major security incidents. Echo Protocol lost ~$76.7M due to an administrator key compromise that enabled unauthorized minting of unbacked eBTC on Monad, StablR suffered ~$12.8M from a multisig governance breach leading to unauthorized stablecoin issuance, and the Verus-Ethereum Bridge incurred ~$11.7M following a type-validation failure that allowed a crafted supplemental export to be misclassified as a valid primary export.

~$16M Lost: DxSale, SquidRouterModule & More | BlockSec Weekly
Security Insights

~$16M Lost: DxSale, SquidRouterModule & More | BlockSec Weekly

This weekly security report covers 5 notable attack incidents between May 25 and May 31, 2026, with combined losses of approximately $16M across BNB Chain, Ethereum, Base, Arbitrum, and Cosmos. Key incidents include the DxSale token locker exploit ($7.3M) involving three missing state updates compounded by a deployer key compromise, the SquidRouterModule exploit ($3.2M) caused by improper input validation in an Axelar Bridge integration that allowed forged cross-chain messages to drain 86 Safe wallets, and the Gravity Bridge signing key compromise ($5.4M). Other incidents involve a compromised deployer key (Stake DAO, $91K) and a vulnerable off-chain bridge backend (Alephium, $300K).

Best Security Auditor for Web3

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

BlockSec Audit