Best Practices

15 Common Solidity Anti-Patterns That Lead to Catastrophic Vulnerabilities

Kennedy OwiroNovember 14, 202511 min read

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.

Solidityanti-patternsbest practicescode qualitysmart contract security
Share

Written by

Kennedy Owiro

Founder & CTO, Vultbase

14+ years building security and QA systems at scale. Background in fintech security and Web3 smart contract testing. Built Vultbase's Intelligence Engine with 1,200+ exploit patterns from $40B+ in historical DeFi losses.

Protect your protocol before launch.

Submit your smart contracts for automated security analysis powered by 1,200+ real exploit patterns.

Start Your Audit →