Smart contract exploits cost the DeFi ecosystem over $1.7 billion in 2024. Understanding the most common vulnerability patterns is the first step toward writing safer code.
1. Reentrancy Attacks
Reentrancy remains the most notorious smart contract vulnerability. An attacker can repeatedly call back into your contract before the first execution finishes, draining funds before balances are updated.
Vulnerable pattern:
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool ok,) = msg.sender.call{value: amount}(""); // ← external call first
require(ok);
balances[msg.sender] -= amount; // ← state update too late
}
Safe pattern (Checks-Effects-Interactions):
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // ← update state first
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
}
Use OpenZeppelin's ReentrancyGuard for an additional layer of protection.
2. Access Control Failures
Missing or misconfigured access controls accounted for the largest losses in 2024 ($953M). Critical functions left unprotected allow anyone to call them directly.
// ❌ No access control
function setAdmin(address newAdmin) external {
admin = newAdmin;
}
// ✅ Protected
function setAdmin(address newAdmin) external onlyOwner {
admin = newAdmin;
}
Watch out for tx.origin checks — they can be bypassed via a malicious intermediary contract. Always use msg.sender.
3. Flash Loan Price Manipulation
Flash loans allow borrowing large sums within a single transaction. If your protocol reads price from an on-chain AMM spot price, an attacker can manipulate it momentarily to drain your treasury.
Prevention: Use time-weighted average prices (TWAP) from Uniswap v3, or a decentralized oracle like Chainlink.
4. Integer Overflow / Underflow
Pre-Solidity 0.8.0, arithmetic didn't revert on overflow. Many legacy contracts still run under older compiler versions.
// Solidity < 0.8.0, this wraps to type(uint).max
uint256 balance = 0;
balance -= 1; // ← underflows silently
Fix: Use Solidity 0.8.x (safe math by default) or OpenZeppelin's SafeMath for older code.
5. Unprotected Initializers
Upgradeable proxy contracts often use initialize() instead of constructor(). If the initializer isn't protected with initializer modifier, an attacker can re-initialize the contract and take ownership.
// ❌ Callable by anyone, anytime
function initialize(address _owner) public {
owner = _owner;
}
// ✅ One-time call enforced
function initialize(address _owner) public initializer {
owner = _owner;
}
How ContractScan Helps
ContractScan detects all five of these patterns in seconds. Try the scanner{: target="_blank"} on your next contract before deployment.