30 Common Security Mistakes Solidity Developers Make (And How to Avoid Them)
Smart contract bugs have cost billions of dollars. This guide covers the 30 most common security mistakes Solidity developers make, with real-world consequences and concrete solutions to prevent them in your code.
Critical Vulnerabilities
These mistakes have led to the largest exploits in DeFi history. Avoiding them is non-negotiable.
Missing Reentrancy Protection
Making external calls before updating state variables, allowing attackers to re-enter and drain funds.
Unchecked External Call Returns
Ignoring return values from low-level calls like.call().delegatecall(), or.send().
Oracle Price Manipulation
Using spot prices from AMMs without TWAP or multiple oracle sources.
Missing Access Control
Leaving admin functions without proper onlyOwner or role-based modifiers.
Uninitialized Proxy Implementation
Deploying upgradeable contracts without calling the initializer or protecting it.
Delegatecall to Untrusted Contract
Using delegatecall with a user-controlled address, allowing arbitrary code execution.
tx.origin for Authorization
Using tx.origin instead of msg.sender for access control.
Missing Slippage Protection
Not allowing users to specify minimum output amounts in swaps or deposits.
Integer Overflow in Pre-0.8 Solidity
Using Solidity <0.8 without SafeMath library for arithmetic operations.
Insufficient Validation of Merkle Proofs
Not validating that Merkle leaves are formatted correctly, allowing proof reuse.
Major Security Issues
These mistakes are frequently exploited and can lead to significant fund loss.
Using transfer() or send() for ETH
Using.transfer() or.send() which forward only 2300 gas, failing on some contracts.
Hardcoded Gas Values
Hardcoding gas amounts that may become insufficient after EVM upgrades.
Unsafe Type Casting
Casting between types without checking for truncation (e.g., uint256 to uint128).
Missing Zero Address Check
Not validating that critical addresses (owner, fee recipient) aren't address(0).
Signature Replay Attacks
Not including nonces or chainId in signed messages, allowing reuse across chains/transactions.
Block Timestamp Manipulation
Relying on block.timestamp for precise timing in high-stakes situations.
Front-Running Vulnerable Approvals
Changing ERC20 allowance without first setting to zero.
Denial of Service via Block Gas Limit
Writing loops that iterate over unbounded arrays.
Incorrect Inheritance Order
Wrong order of inherited contracts leading to unexpected function resolution.
Storage Collision in Upgrades
Changing storage layout between proxy upgrades, corrupting existing data.
Common Code Quality Issues
These mistakes may not directly cause exploits but indicate poor security practices.
Using Floating Pragma
Using pragma solidity ^0.8.0 instead of a fixed version.
Missing Events for State Changes
Not emitting events when critical state variables change.
Public Functions That Should Be External
Using public visibility when external would suffice.
Unused Return Values
Calling functions without using their return values.
Missing NatSpec Documentation
Not documenting functions, parameters, and security assumptions.
Using ecrecover Directly
Using raw ecrecover without handling edge cases and malleability.
Inconsistent Error Handling
Mixing require, revert, and assert without clear purpose.
Magic Numbers in Code
Using raw numbers like 10000 for basis points without constants.
Not Using SafeERC20
Calling ERC20 transfer/approve directly without handling non-standard tokens.
Ignoring Compiler Warnings
Deploying contracts with unresolved compiler warnings.
Key Recommendations
Keep a personal 'bugs I've made' document. Reviewing your past mistakes is the fastest way to stop repeating them.
Before every deployment, ask yourself: 'How would I attack this contract if I had unlimited capital for one block?'
Follow audit reports from Trail of Bits, OpenZeppelin, and Spearbit. Each report teaches you new attack patterns.
Use Foundry's console.log liberally during development, but remove all logs before deployment to save gas.
When you find a bug, don't just fix it. Add a test that would have caught it. Build your test suite from real bugs.
