Common Vulnerabilities

Difficulty: Advanced

Smart contract security is fundamentally different from traditional software security because deployed contracts are immutable (unless behind a proxy), handle real financial value directly, and operate in an adversarial environment where every transaction is visible to potential attackers. A single vulnerability can result in the irreversible loss of millions of dollars, as demonstrated by numerous high-profile exploits throughout DeFi history. The Smart Contract Weakness Classification (SWC) registry, modeled after MITRE's CWE system, provides a standardized taxonomy of known vulnerability patterns that every Solidity developer should understand.

The top vulnerabilities in smart contracts can be broadly categorized into several classes. Logic errors include incorrect access control, flawed business logic, and improper state management. Reentrancy vulnerabilities allow an attacker to re-enter a function before its first invocation completes, potentially draining funds. Integer issues include overflow/underflow (largely mitigated in Solidity 0.8+) and precision loss in arithmetic. External call risks include unchecked return values, delegatecall to untrusted contracts, and denial-of-service through external call failures. Front-running and MEV-related vulnerabilities allow miners or other users to exploit transaction ordering for profit.

The SWC registry catalogs over 30 distinct vulnerability types. Some of the most critical include: SWC-107 (Reentrancy), SWC-101 (Integer Overflow and Underflow), SWC-115 (Authorization through tx.origin), SWC-104 (Unchecked Call Return Value), SWC-106 (Unprotected SELFDESTRUCT), SWC-112 (Delegatecall to Untrusted Callee), SWC-116 (Block Values as a Source of Randomness), and SWC-105 (Unprotected Ether Withdrawal). Each entry includes a description, remediation guidance, and test cases. Developers should familiarize themselves with the full registry as a checklist during code review.

Beyond individual vulnerability types, smart contract security requires a holistic approach. Defense-in-depth involves multiple layers of protection: input validation, access control, reentrancy guards, pausability, timelocks for sensitive operations, and monitoring. The principle of least privilege means functions should only have the minimum permissions necessary. Fail-safe defaults mean that the contract should default to the most restrictive state - for example, if an oracle fails, the contract should pause rather than use a stale price. Code simplicity is also a security principle - complex code has more attack surface, so simpler designs are generally more secure.

Real-world examples illustrate the devastating impact of these vulnerabilities. The DAO hack (2016) exploited reentrancy to drain $60M of ETH, leading to the Ethereum hard fork. The Parity multisig wallet bug (2017) allowed an attacker to take ownership of the library contract and self-destruct it, freezing $280M permanently. The Wormhole bridge exploit (2022) exploited an unvalidated input in the Solana-side verification, allowing $320M in fabricated wETH to be minted. Each of these exploits could have been prevented with proper security practices: reentrancy guards, access control on critical functions, and thorough input validation.

Code examples

Common Vulnerability Patterns (Anti-Patterns)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// ======== VULNERABLE CONTRACT (DO NOT USE) ========
// Demonstrates common vulnerability patterns for education

contract VulnerableVault {
    mapping(address => uint256) public balances;
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // VULNERABILITY 1: Reentrancy
    function withdrawUnsafe(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient");
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        balances[msg.sender] -= amount; // State update AFTER external call!
    }

    // VULNERABILITY 2: tx.origin authentication
    function transferOwnerUnsafe(address newOwner) external {
        require(tx.origin == owner, "Not owner"); // Phishable!
        owner = newOwner;
    }

    // VULNERABILITY 3: Unchecked external call
    function sendEtherUnsafe(address payable to, uint256 amount) external {
        to.send(amount); // Return value ignored - silent failure!
    }

    // VULNERABILITY 4: Denial of Service via push pattern
    address[] public recipients;
    function distributeUnsafe() external {
        for (uint256 i = 0; i < recipients.length; i++) {
            (bool success, ) = recipients[i].call{value: 1 ether}("");
            require(success); // One failing recipient blocks all others
        }
    }

    // VULNERABILITY 5: Front-running susceptible
    bytes32 public commitHash;
    function revealUnsafe(string memory secret) external {
        require(keccak256(abi.encodePacked(secret)) == commitHash);
        // Anyone watching the mempool can see the secret
    }

    receive() external payable {
        balances[msg.sender] += msg.value;
    }
}

This contract demonstrates five common vulnerability patterns: reentrancy (state update after external call), tx.origin authentication (phishable), unchecked external calls, DoS via push pattern, and front-running susceptibility. Each is labeled with comments explaining the bug. NEVER use these patterns in production.

Secure Vault (Fixed Patterns)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract SecureVault is ReentrancyGuard, Ownable, Pausable {
    mapping(address => uint256) public balances;
    mapping(address => uint256) public pendingWithdrawals;

    event Deposit(address indexed user, uint256 amount);
    event Withdrawal(address indexed user, uint256 amount);

    constructor() Ownable(msg.sender) {}

    // FIX 1: Checks-Effects-Interactions + ReentrancyGuard
    function withdraw(uint256 amount) external nonReentrant whenNotPaused {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        require(amount > 0, "Amount must be > 0");

        balances[msg.sender] -= amount; // Effects BEFORE interactions

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        emit Withdrawal(msg.sender, amount);
    }

    // FIX 2: Pull pattern instead of push (prevents DoS)
    function allocatePayment(address recipient, uint256 amount) external onlyOwner {
        pendingWithdrawals[recipient] += amount;
    }

    function claimPayment() external nonReentrant {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "Nothing to claim");
        pendingWithdrawals[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }

    // FIX 3: Commit-reveal for front-running protection
    mapping(address => bytes32) public commits;
    mapping(address => uint256) public commitBlocks;

    function commit(bytes32 hash) external {
        commits[msg.sender] = hash;
        commitBlocks[msg.sender] = block.number;
    }

    function reveal(string memory secret, uint256 nonce) external {
        require(block.number > commitBlocks[msg.sender] + 2, "Too early");
        require(
            keccak256(abi.encodePacked(msg.sender, secret, nonce)) == commits[msg.sender],
            "Invalid reveal"
        );
        delete commits[msg.sender];
    }

    // FIX 4: Emergency pause
    function pause() external onlyOwner { _pause(); }
    function unpause() external onlyOwner { _unpause(); }

    function deposit() external payable whenNotPaused {
        require(msg.value > 0, "Must send ETH");
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    receive() external payable {
        balances[msg.sender] += msg.value;
    }
}

This contract fixes each vulnerability: Checks-Effects-Interactions with ReentrancyGuard prevents reentrancy, Ownable uses msg.sender (not tx.origin), pull payment pattern prevents DoS, commit-reveal prevents front-running, and Pausable provides emergency stop capability.

Security Checklist for Smart Contract Review

// JavaScript: Automated security checklist
const securityChecklist = [
  {
    category: 'Reentrancy',
    checks: [
      'External calls follow checks-effects-interactions pattern',
      'ReentrancyGuard on all functions with external calls and state changes',
      'Cross-function reentrancy via shared state analyzed',
    ],
  },
  {
    category: 'Access Control',
    checks: [
      'All sensitive functions have proper modifiers (onlyOwner, onlyRole)',
      'msg.sender used instead of tx.origin for authentication',
      'Function visibility is explicit and minimal',
      'Initializer functions protected (for proxies)',
    ],
  },
  {
    category: 'Input Validation',
    checks: [
      'All external parameters validated',
      'Address parameters checked for address(0)',
      'Array lengths bounded to prevent out-of-gas',
    ],
  },
  {
    category: 'Arithmetic',
    checks: [
      'Solidity 0.8+ for overflow protection',
      'unchecked blocks only where overflow is provably impossible',
      'Multiplication before division (precision)',
      'Type casts validated for truncation',
    ],
  },
  {
    category: 'External Calls',
    checks: [
      'Return values of low-level calls checked',
      'No delegatecall to untrusted addresses',
      'Pull over push for ETH distributions',
    ],
  },
  {
    category: 'Oracle & DeFi',
    checks: [
      'Oracle data validated (freshness, positivity, round completeness)',
      'No reliance on spot DEX prices',
      'Flash loan attack vectors considered',
      'Slippage protection on swaps',
    ],
  },
];

function printChecklist(contractName) {
  console.log(`Security Checklist: ${contractName}`);
  console.log('='.repeat(50));
  let total = 0;
  for (const section of securityChecklist) {
    console.log(`\n[${section.category}]`);
    for (const check of section.checks) {
      total++;
      console.log(`  [ ] ${check}`);
    }
  }
  console.log(`\nTotal checks: ${total}`);
}

printChecklist('MyProtocol');

This checklist covers the most critical security areas for smart contract review. Use it as a framework for manual review and customize based on the protocol type. Automated tools handle some checks, but many require manual analysis.

Key points

Concepts covered

Smart Contract Security, SWC Registry, Vulnerability Classification, Attack Surface, Security Best Practices