Hashtag Web3 Logo

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.

For: solidity developerUpdated: March 13, 2026

Critical Vulnerabilities

These mistakes have led to the largest exploits in DeFi history. Avoiding them is non-negotiable.

Missing Reentrancy Protection

critical

Making external calls before updating state variables, allowing attackers to re-enter and drain funds.

What happens: The DAO hack ($60M), Cream Finance ($130M), and countless others exploited this exact pattern.

Fix: Always use checks-effects-interactions pattern AND ReentrancyGuard. Update all state before external calls.

Unchecked External Call Returns

critical

Ignoring return values from low-level calls like .call(), .delegatecall(), or .send().

What happens: Failed transfers silently succeed, leading to accounting errors and fund loss.

Fix: Always check return values: (bool success, ) = addr.call{value: amount}(""); require(success);

Oracle Price Manipulation

critical

Using spot prices from AMMs without TWAP or multiple oracle sources.

What happens: Attackers use flashloans to manipulate prices within a single transaction. Hundreds of millions lost.

Fix: Use Chainlink or other decentralized oracles. For AMM prices, use TWAP with sufficient window.

Missing Access Control

critical

Leaving admin functions without proper onlyOwner or role-based modifiers.

What happens: Anyone can call privileged functions, drain funds, or take over the contract.

Fix: Use OpenZeppelin's AccessControl or Ownable2Step. Audit every external/public function.

Uninitialized Proxy Implementation

critical

Deploying upgradeable contracts without calling the initializer or protecting it.

What happens: Attackers call initialize() and become owner. Wormhole lost $320M to a variant of this.

Fix: Call _disableInitializers() in the constructor and ensure initialize is called during deployment.

Delegatecall to Untrusted Contract

critical

Using delegatecall with a user-controlled address, allowing arbitrary code execution.

What happens: Attacker executes malicious code in your contract's context, taking full control.

Fix: Never delegatecall to addresses that users can influence. Whitelist allowed targets.

tx.origin for Authorization

critical

Using tx.origin instead of msg.sender for access control.

What happens: Phishing attacks can trick users into calling malicious contracts that relay to yours.

Fix: Always use msg.sender for authorization. tx.origin should almost never be used.

Missing Slippage Protection

critical

Not allowing users to specify minimum output amounts in swaps or deposits.

What happens: Sandwich attacks extract value from every transaction. Users lose to MEV bots.

Fix: Add minAmountOut parameters to all trading functions. Let users set their slippage tolerance.

Integer Overflow in Pre-0.8 Solidity

critical

Using Solidity <0.8 without SafeMath library for arithmetic operations.

What happens: Overflows wrap around, allowing attackers to create tokens from nothing or bypass checks.

Fix: Use Solidity 0.8+ or SafeMath for all arithmetic. Be careful with unchecked blocks.

Insufficient Validation of Merkle Proofs

critical

Not validating that Merkle leaves are formatted correctly, allowing proof reuse.

What happens: Attackers claim multiple times or claim on behalf of others.

Fix: Hash leaves with abi.encodePacked(msg.sender, amount) and mark claimed addresses.

Major Security Issues

These mistakes are frequently exploited and can lead to significant fund loss.

Using transfer() or send() for ETH

major

Using .transfer() or .send() which forward only 2300 gas, failing on some contracts.

What happens: Transfers to contracts with receive() functions that need more gas will fail permanently.

Fix: Use .call{value: amount}("") with reentrancy protection instead.

Hardcoded Gas Values

major

Hardcoding gas amounts that may become insufficient after EVM upgrades.

What happens: Functions become unusable after gas repricing, potentially locking funds.

Fix: Avoid hardcoded gas values. Test with different gas prices and EVM versions.

Unsafe Type Casting

major

Casting between types without checking for truncation (e.g., uint256 to uint128).

What happens: Large values get truncated, leading to incorrect calculations and potential exploits.

Fix: Use SafeCast library or explicit bounds checking before downcasting.

Missing Zero Address Check

major

Not validating that critical addresses (owner, fee recipient) aren't address(0).

What happens: Funds sent to zero address are burned. Ownership set to zero address locks the contract.

Fix: Add require(addr != address(0)) for all address parameters in setters and constructors.

Signature Replay Attacks

major

Not including nonces or chainId in signed messages, allowing reuse across chains/transactions.

What happens: Valid signatures can be replayed, allowing unauthorized actions.

Fix: Use EIP-712 with chainId and per-user nonces. Increment nonce after each use.

Block Timestamp Manipulation

major

Relying on block.timestamp for precise timing in high-stakes situations.

What happens: Miners can manipulate timestamp by several seconds, affecting outcomes.

Fix: Use block numbers for precision. Allow ~15 second tolerance for timestamp-based logic.

Front-Running Vulnerable Approvals

major

Changing ERC20 allowance without first setting to zero.

What happens: Attacker front-runs and spends both old and new allowance.

Fix: Use increaseAllowance/decreaseAllowance or set to 0 first, then new value.

Denial of Service via Block Gas Limit

major

Writing loops that iterate over unbounded arrays.

What happens: Array grows until processing exceeds block gas limit, permanently bricking the contract.

Fix: Use pagination, caps, or pull patterns instead of push patterns with loops.

Incorrect Inheritance Order

major

Wrong order of inherited contracts leading to unexpected function resolution.

What happens: Wrong implementation gets called, potentially bypassing security checks.

Fix: List base contracts from most base-like to most derived. Test inheritance carefully.

Storage Collision in Upgrades

major

Changing storage layout between proxy upgrades, corrupting existing data.

What happens: Variables point to wrong storage slots, leading to data corruption and exploits.

Fix: Only append new variables. Use storage gaps. Run upgrade-safety checks.

Common Code Quality Issues

These mistakes may not directly cause exploits but indicate poor security practices.

Using Floating Pragma

minor

Using pragma solidity ^0.8.0 instead of a fixed version.

What happens: Contract may compile with untested compiler version introducing bugs.

Fix: Lock pragma to specific version you've tested: pragma solidity 0.8.20;

Missing Events for State Changes

minor

Not emitting events when critical state variables change.

What happens: Makes monitoring and debugging difficult. Hides malicious admin actions.

Fix: Emit events for all state changes, especially in admin functions.

Public Functions That Should Be External

minor

Using public visibility when external would suffice.

What happens: Wastes gas and signals lack of attention to detail in security reviews.

Fix: Use external for functions only called from outside. Reserve public for internal calls.

Unused Return Values

minor

Calling functions without using their return values.

What happens: May miss important status information or error conditions.

Fix: Either use the return value or explicitly discard: (, uint b) = func();

Missing NatSpec Documentation

minor

Not documenting functions, parameters, and security assumptions.

What happens: Makes auditing harder, increases chance of misuse by integrators.

Fix: Add @notice, @param, @return, and @dev tags to all external functions.

Using ecrecover Directly

minor

Using raw ecrecover without handling edge cases and malleability.

What happens: Signature malleability can lead to duplicate valid signatures.

Fix: Use OpenZeppelin's ECDSA library which handles all edge cases.

Inconsistent Error Handling

minor

Mixing require, revert, and assert without clear purpose.

What happens: assert uses all remaining gas. Confuses intention of error handling.

Fix: Use require for input validation, assert only for invariants, custom errors for gas efficiency.

Magic Numbers in Code

minor

Using raw numbers like 10000 for basis points without constants.

What happens: Makes code harder to audit and maintain. Easy to introduce errors.

Fix: Define constants: uint256 constant BASIS_POINTS = 10000;

Not Using SafeERC20

major

Calling ERC20 transfer/approve directly without handling non-standard tokens.

What happens: Some tokens don't return bool, causing transfers to fail silently.

Fix: Use OpenZeppelin's SafeERC20 for all token interactions.

Ignoring Compiler Warnings

minor

Deploying contracts with unresolved compiler warnings.

What happens: Warnings often indicate real issues that could become vulnerabilities.

Fix: Treat warnings as errors. Resolve all warnings before deployment.

Tips from the field

  • 1.

    Keep a personal 'bugs I've made' document. Reviewing your past mistakes is the fastest way to stop repeating them.

  • 2.

    Before every deployment, ask yourself: 'How would I attack this contract if I had unlimited capital for one block?'

  • 3.

    Follow audit reports from Trail of Bits, OpenZeppelin, and Spearbit. Each report teaches you new attack patterns.

  • 4.

    Use Foundry's console.log liberally during development, but remove all logs before deployment to save gas.

  • 5.

    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.

Every exploit in DeFi history was caused by one of these mistakes or a variation. Learn them, internalize them, and make checking for them a habitual part of your development process.

Related reading

More for solidity developer