After auditing thousands of smart contracts, certain anti-patterns appear again and again. These aren't obscure edge cases — they're common mistakes that experienced developers make repeatedly. Here are the 15 most dangerous Solidity anti-patterns and how to fix each one.
1. State Changes After External Calls
// ❌ BAD: State change after external call
(bool s,) = to.call{value: amt}("");
balances[msg.sender] -= amt;
// ✅ GOOD: CEI pattern
balances[msg.sender] -= amt;
(bool s,) = to.call{value: amt}("");
2. Using tx.origin for Authentication
// ❌ BAD
require(tx.origin == owner);
// ✅ GOOD
require(msg.sender == owner);
3. Unbounded Loops
// ❌ BAD: Array grows unbounded
for (uint i = 0; i < users.length; i++) { ... }
// ✅ GOOD: Paginated processing
function process(uint start, uint count) external { ... }
4. Missing Zero-Address Checks
// ❌ BAD: Owner set to address(0) permanently
function setOwner(address newOwner) external onlyOwner {
owner = newOwner;
}
// ✅ GOOD
require(newOwner != address(0), "Zero address");
5. Public Functions That Should Be External
Use external for functions only called from outside — it's cheaper for calldata parameters and makes the interface clearer.
6. Hardcoded Gas Limits
// ❌ BAD: Gas costs change with EVM upgrades
payable(to).transfer(amount); // 2300 gas limit
// ✅ GOOD: Forward all gas
(bool s,) = to.call{value: amount}("");
7. Missing Event Emissions
State changes without events make monitoring impossible and debugging a nightmare. Emit events for every significant state change.
8. Division Before Multiplication
// ❌ BAD: Precision loss
uint share = amount / totalSupply * 1e18;
// ✅ GOOD: Multiply first
uint share = amount * 1e18 / totalSupply;
9. Missing Slippage Protection
// ❌ BAD: No minimum output
function swap(uint amountIn) external { ... }
// ✅ GOOD: User-defined minimum
function swap(uint amountIn, uint minAmountOut) external { ... }
10. Floating Pragma
// ❌ BAD: Can compile with any version
pragma solidity ^0.8.0;
// ✅ GOOD: Locked version
pragma solidity 0.8.24;
11. Not Using SafeERC20
Direct IERC20.transfer() calls fail silently with USDT and other non-standard tokens. Always use SafeERC20.
12. Single-Step Ownership Transfer
// ❌ BAD: One typo = lost ownership
function transferOwnership(address newOwner) external { owner = newOwner; }
// ✅ GOOD: Two-step transfer
function transferOwnership(address newOwner) external { pendingOwner = newOwner; }
function acceptOwnership() external { require(msg.sender == pendingOwner); }
13. Using block.timestamp for Critical Logic
Validators can manipulate by ~12 seconds. Use block.number or Chainlink VRF for security-sensitive operations.
14. Unprotected Initializers
Forgetting the initializer modifier on proxy implementation contracts allows anyone to assume ownership.
15. Missing Reentrancy Guards on Token Callbacks
ERC-721 safeTransfer, ERC-777 hooks, and ERC-1155 callbacks all enable reentrancy — not just ETH transfers.
Every anti-pattern on this list has caused real losses. Audit your contracts and catch these before deployment.