blogs

Smart Contract Security: The Vulnerabilities That Have Cost Billions of Dollars

The smart contract vulnerability classes -- from reentrancy to oracle manipulation -- that have collectively cost the blockchain industry billions.

Aug 2, 2024
blockchainsmart-contractsweb3-securitysolidity
Smart Contract Security: The Vulnerabilities That Have Cost Billions of Dollars

Preview

Smart Contract Security: The Vulnerabilities That Have Cost Billions of Dollars

Blockchain is often described as immutable and trustless — and in many ways it is. But smart contracts, the code that runs on blockchains, are written by humans. And humans write bugs. The difference between a bug in a web app and a bug in a smart contract is stark: when a web app has a bug, you patch it. When a smart contract has a bug, and there's money locked in it, that money can be gone forever.

The history of DeFi is littered with hacks that exploited smart contract vulnerabilities. The DAO hack (2016) drained $60 million. The Ronin Network hack (2022) took $625 million. Poly Network (2021) saw $611 million stolen — though most was later returned. These aren't edge cases. They're the inevitable result of deploying complex financial logic on an immutable medium without adequate security review.

This post covers the most critical smart contract vulnerability classes, with code examples and mitigation strategies.

Reentrancy Attacks

This is the vulnerability class that caused the DAO hack and remains one of the most dangerous in smart contract development.

How it works: A smart contract sends Ether to an external address. If that external address is itself a contract, it can call back into the original contract before the first call finishes executing. If the original contract updates its state (like reducing a balance) after the external call, the attacker can repeatedly drain funds before the balance is ever reduced.

Vulnerable code:

// VULNERABLE - Do not use
contract VulnerableBank {
    mapping(address => uint) public balances;

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        
        // This sends ETH to the caller — if caller is a contract, it can re-enter
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        
        // State update happens AFTER external call — too late
        balances[msg.sender] -= amount;
    }
}

Attacker contract:

contract Attacker {
    VulnerableBank public bank;
    
    receive() external payable {
        // Called when ETH arrives — re-enter before balance updates
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);
        }
    }
    
    function attack() external {
        bank.withdraw(1 ether); // Initial call
    }
}

Mitigation — Checks-Effects-Interactions pattern:

// SAFE - Checks-Effects-Interactions
contract SafeBank {
    mapping(address => uint) public balances;

    function withdraw(uint amount) public {
        // Checks
        require(balances[msg.sender] >= amount, "Insufficient balance");
        
        // Effects — update state BEFORE external interaction
        balances[msg.sender] -= amount;
        
        // Interactions — external call comes last
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

You can also use OpenZeppelin's ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SafeBank is ReentrancyGuard {
    function withdraw(uint amount) public nonReentrant {
        // ...
    }
}

Integer Overflow and Underflow

Before Solidity 0.8.0, arithmetic operations could silently wrap around. An addition that exceeded the maximum value of uint256 would wrap to zero. A subtraction below zero would wrap to the maximum value.

Vulnerable example (Solidity < 0.8.0):

// In Solidity < 0.8.0
uint8 balance = 0;
balance -= 1; // Wraps to 255 — attacker has a massive balance

Mitigation: Use Solidity 0.8.0 or higher (arithmetic reverts on overflow/underflow by default), or use OpenZeppelin's SafeMath library for older versions.

Access Control Vulnerabilities

Many high-profile hacks have exploited missing or incorrect access controls — functions that should be admin-only but aren't restricted, or initialization functions that can be called by anyone after deployment.

Vulnerable example:

contract VulnerableToken {
    address public owner;
    mapping(address => uint) public balances;
    
    // Anyone can call this and mint unlimited tokens
    function mint(address to, uint amount) public {
        balances[to] += amount;
    }
    
    // Missing onlyOwner modifier
    function setOwner(address newOwner) public {
        owner = newOwner;
    }
}

Mitigation:

import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureToken is Ownable {
    mapping(address => uint) public balances;
    
    function mint(address to, uint amount) public onlyOwner {
        balances[to] += amount;
    }
}

Use OpenZeppelin's Ownable, AccessControl, or Roles libraries rather than rolling your own access control logic.

Oracle Manipulation

Smart contracts often need external data — asset prices, random numbers, event outcomes. They get this from oracles. A vulnerable oracle design can be manipulated, often within a single transaction using a flash loan.

The classic price oracle attack:

  1. Attacker takes a large flash loan
  2. Uses the loan to massively buy an asset on a DEX, spiking its price
  3. Uses that inflated price (as read by an on-chain oracle from the DEX) to borrow far more than the collateral is worth from a lending protocol
  4. Repays flash loan, walks away with the difference

Mitigation: Use time-weighted average prices (TWAPs) rather than spot prices from single DEX pools. Use decentralized oracle networks like Chainlink that aggregate prices from many sources.

Front-Running

On public blockchains, all pending transactions are visible in the mempool before they're confirmed. Bots (and sophisticated traders) monitor the mempool for profitable transactions and submit their own transactions with higher gas fees to execute before the target transaction.

This matters for:

  • DEX trades (sandwich attacks — buy before and sell after a large trade)
  • NFT mints
  • Any time-sensitive state transitions

Mitigation: Use commit-reveal schemes where users first commit to a hash of their action and reveal later. Use private mempools (like Flashbots) for sensitive transactions.

Unchecked Return Values

Solidity's low-level call(), send(), and transfer() functions return booleans indicating success or failure. Ignoring these return values means your contract silently continues execution even when a critical operation failed.

// VULNERABLE — success is ignored
someAddress.send(amount);

// SAFE — check the return value
(bool success, ) = someAddress.call{value: amount}("");
require(success, "Transfer failed");

What I Learned from the Solana Hacker House

I had the opportunity to present at the Solana Hacker House, where I got to discuss blockchain security with developers building on Solana. One thing that struck me: Solana's programming model (using Rust, accounts-based architecture) introduces different security patterns than EVM contracts, but many of the same categories of vulnerabilities apply — access control issues, arithmetic errors, and oracle trust assumptions.

The lesson isn't that any particular blockchain is more secure. The lesson is that security requires deliberate effort regardless of the platform.

Security Tools for Smart Contracts

If you're developing smart contracts, these tools should be in your workflow:

  • Slither (Trail of Bits): Static analysis tool that detects dozens of vulnerability classes automatically
  • Mythril: Symbolic execution tool for finding security issues
  • Echidna: Property-based fuzzer — you define invariants and it tries to break them
  • OpenZeppelin Defender: Monitoring and incident response for deployed contracts
  • Foundry: Testing framework that makes writing security tests much easier

No tool catches everything. Manual review by a security-focused auditor is still essential for contracts holding significant value.

The Human Factor

All of the vulnerabilities I've described are technically preventable. But they keep appearing because smart contract development moves fast, security audits are expensive, and developers are often focused on functionality over security.

The DeFi ecosystem has gotten better at this — security audits are now expected for any serious protocol, bug bounties are common, and tools like Slither are widely used. But the fundamental tension between speed-to-market and security rigor will always create opportunities for vulnerabilities.

If you're building on blockchain, treat your smart contracts like you'd treat code running a nuclear reactor: assume failure modes exist, design for them, test exhaustively, and consider formal verification for the most critical components.


I presented blockchain and cybersecurity projects at REVA Hackathons and the Solana Hacker House. Security analysis of smart contract architectures is an area I'm actively developing expertise in.