Reentrancy is one of the oldest and most documented attack vectors in smart contract security. And yet — in 2025 — it’s still popping up in real-world audits, bug bounties, and mainnet exploits.
This post isn’t a tutorial. It’s a pattern review. A breakdown of where developers still get it wrong, and how subtle variations of reentrancy continue to break production protocols.
You’ve seen this one in every blog post since The DAO hack (2016), but developers still write it:
function withdraw() public {
uint amount = balances[msg.sender];
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");
balances[msg.sender] = 0;
}
🧠 What’s wrong?
State is updated after external call. If the receiving contract is malicious, it can recursively call withdraw() before balances[msg.sender] is reset.
function withdraw() public {
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");
}


