← Back to Blog

Top 5 Solidity Vulnerabilities in 2024

2024-11-15 security solidity defi vulnerabilities

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.