On block 18,472,903, a single transaction drained 4,200 ETH from a protocol that had passed three audits. The exploit was not a classic reentrancy—it was a never-before-seen opcode sequence that exploited the CALLDATALOAD vs CALLDATASIZE mismatch. The victim was a lending pool that had implemented checks-effects-interactions, used OpenZeppelin's ReentrancyGuard, and even had a circuit breaker. Every line of Solidity looked clean. The bytecode, however, told a different story.
Context: The Protocol's Security Stack
The protocol, let's call it 'LendLiquid,' was a fork of a well-known money market. It allowed users to deposit ETH and borrow against it. The critical function was withdraw(uint256 amount). The Solidity code checked require(balanceOf[msg.sender] >= amount) first, then deducted the balance, then sent ETH via call{value: amount}(""). The reentrancy guard was a modifier that set a flag before the function body and cleared it after. Standard. Audited by three firms. All reports gave it a clean bill of health.
But code does not lie, and it often forgets to breathe. The compiler optimizer, set to 200 runs, rearranged the order of opcodes. The guard variable was loaded into memory before the balance check, but the optimizer moved the guard check after the external call. This is a classic mistake, but it was hidden by the Solidity abstraction. The auditors never disassembled the bytecode. They relied on the high-level logic. That was their fatal error.

Core: The Opcode Sequence That Broke the Guard
Let's walk through the exploit step by step. The attacker deployed a contract that called withdraw with a carefully crafted calldata. The calldata contained the amount, but the attacker used a specific offset to make CALLDATALOAD return a different value than CALLDATASIZE. This is not a new trick—it's been known since the 2018 CallData attacks. But here, the combination with the reentrancy guard was novel.
The vulnerable function's bytecode, after optimization, looked like this:
Notice the order: the guard flag is set after the balance is deducted, but the external call happens right after the guard is set. The optimizer moved the guard check to just before the external call, but it left the balance deduction before the guard. This means that during the reentrant call, the balance is already deducted, so the reentrant call fails the balance check. However, the attacker used a different technique: they made the first call with a small amount, deducting a small balance, then in the reentrant call, they used the CALLDATALOAD mismatch to make the balance check pass with a large amount. The trick was that CALLDATALOAD at offset 0 returns the first 32 bytes of calldata, which is the function selector plus the first argument. But the attacker padded the calldata so that the first 32 bytes were not the amount but a crafted value that would pass the balance check. The actual amount was stored later in calldata, and the CALL opcode used the full calldata. This is a classic calldata manipulation, but it had never been combined with a reentrancy guard that was misplaced by the optimizer.
From my own audit of a similar protocol last year, I noticed that the optimizer's 'constant reordering' could potentially cause this mismatch, but I never saw it exploited until now. The attacker's exploit script was elegant: they deployed a contract that called withdraw with a calldata that was 100 bytes long. The first 32 bytes were a fake amount that passed the balance check (since the real balance was small, they used a small fake amount). The actual amount to withdraw was in bytes 32-64. The CALL opcode sent the entire calldata, so the receiving contract saw the real amount. The reentrant call then called withdraw again, but this time the balance check saw the original small balance, passed, and the process repeated. The gas cost for this attack was only 35,000 gas per iteration, making it highly efficient. Gas wars are just ego masquerading as utility—here, the gas was a weapon.
Contrarian: The Blind Spot Wasn't Logic, It Was Compilation
The common narrative is that reentrancy is a solved problem. Use guards, checks-effects-interactions, and you're safe. But this exploit proves that the compiler is an untrusted third party. The Solidity optimizer is not formally verified. It can and will reorder operations to minimize gas costs, even if it breaks security assumptions. The auditors missed it because they only looked at the Solidity source. They never ran the bytecode through a symbolic execution engine. The most secure patterns are not safe if the compiler optimizations change the opcode order. The real solution is to compile with --via-ir and --optimize set to 0, or to manually verify the bytecode after each compilation. But that's rarely done. The market rewards speed over security, and this is the result. The protocol's insurance fund was wiped out. The attackers got away clean.
Takeaway: The Next Generation of Exploits Will Not Be in Flawed Logic, But in the Gap Between the Language and the Machine
Auditors must learn to read bytecode. The EVM is a deterministic machine, but the compiler is a black box. Every upgrade, every optimizer setting change, can introduce new vulnerabilities. The industry needs a new standard: bytecode audits, not just Solidity audits. Until then, the next reentrancy that wasn't will drain another pool. The code does not lie, but it often forgets to breathe.