Skip to content
Unverified — AI-generated content. Help verify this page

Web3 & Smart Contract Security

Smart contracts are immutable programs that control billions of dollars in digital assets. Unlike traditional software where you can patch a vulnerability after deployment, a vulnerable smart contract is permanently exploitable unless explicitly designed with upgrade mechanisms. The stakes are absolute: a single bug can drain an entire protocol's treasury in a single transaction.

Since 2016, over $10 billion has been lost to smart contract exploits, bridge hacks, and DeFi protocol attacks. This page covers the vulnerability classes, auditing methodology, real-world case studies, and tools used by smart contract security researchers and auditors.

Related: Cybersecurity Overview | Web App Pentesting | Practical Cryptography | Bug Bounty

Immutable Code, Permanent Risk

Smart contracts cannot be patched after deployment (unless they use a proxy pattern). A vulnerability discovered post-deployment may result in total loss of funds. Security audits before deployment are not optional — they are existential.


Smart Contract Attack Surface


Solidity Vulnerabilities

Reentrancy

The most famous smart contract vulnerability. An external call to an untrusted contract allows that contract to call back into the vulnerable function before the first execution completes.

solidity
// VULNERABLE — state updated after external call
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");

        // External call BEFORE state update — reentrancy vulnerability
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // State updated AFTER external call — attacker re-enters before this line
        balances[msg.sender] = 0;
    }
}

// ATTACKER contract
contract Attacker {
    VulnerableVault public vault;

    constructor(address _vault) {
        vault = VulnerableVault(_vault);
    }

    // Fallback function — called when ETH is received
    receive() external payable {
        if (address(vault).balance >= 1 ether) {
            vault.withdraw();  // Re-enter withdraw() before balance is zeroed
        }
    }

    function attack() external payable {
        vault.deposit{value: 1 ether}();
        vault.withdraw();  // First call — triggers reentrancy loop
    }
}
solidity
// SECURE — Checks-Effects-Interactions pattern
contract SecureVault {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");

        // Effect: update state BEFORE external call
        balances[msg.sender] = 0;

        // Interaction: external call AFTER state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

// Even more secure — use ReentrancyGuard from OpenZeppelin
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureVaultV2 is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Checks-Effects-Interactions Pattern

The fundamental defense against reentrancy:

  1. Checks — Validate all conditions (require statements)
  2. Effects — Update all state variables
  3. Interactions — Make external calls last

Integer Overflow / Underflow

Before Solidity 0.8, arithmetic operations silently wrapped around on overflow/underflow. Since 0.8, overflow reverts by default.

solidity
// Pre-Solidity 0.8 — VULNERABLE
contract VulnerableToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {
        // If balances[msg.sender] = 0 and amount = 1:
        // 0 - 1 = 2^256 - 1 (max uint256) — underflow!
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

// Post-Solidity 0.8 — safe by default (reverts on overflow)
// But 'unchecked' blocks bypass this protection
contract ModernToken {
    mapping(address => uint256) public balances;

    function unsafeTransfer(address to, uint256 amount) external {
        unchecked {
            // DANGEROUS — overflow/underflow not checked
            balances[msg.sender] -= amount;
            balances[to] += amount;
        }
    }
}

Access Control Flaws

Missing or incorrect access control is the simplest but most common vulnerability class.

solidity
// VULNERABLE — anyone can call administrative functions
contract VulnerableAdmin {
    address public owner;
    bool public paused;

    // Missing access control — anyone can change owner
    function setOwner(address newOwner) external {
        owner = newOwner;
    }

    // Missing access control — anyone can drain funds
    function withdrawAll() external {
        payable(msg.sender).transfer(address(this).balance);
    }
}

// SECURE — proper access control
contract SecureAdmin {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    function setOwner(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Zero address");
        owner = newOwner;
    }

    function withdrawAll() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }
}

Front-Running / MEV

Transactions in the mempool are visible before execution. Miners/validators and MEV bots can reorder, insert, or censor transactions for profit.

MEV TypeDescriptionProfit Source
Sandwich AttackFront-run + back-run a swapUser pays higher price
ArbitrageCross-DEX price differencesMarket inefficiency
LiquidationFront-run liquidation callsLiquidation bonus
JIT LiquidityAdd liquidity just-in-time for a large swapSwap fees
Time-banditReorg blocks to steal MEVHistorical transactions

DeFi Exploit Case Studies

Major Exploits Timeline

YearExploitLossVulnerabilityATT&CK
2016The DAO$60MReentrancySmart contract flaw
2020bZx$8MFlash loan + oracle manipulationPrice oracle
2021Poly Network$611MAccess control (recovered)Cross-chain
2022Wormhole Bridge$326MSignature verification bypassBridge
2022Ronin Bridge$625MPrivate key compromise (5/9 validators)Key management
2022Nomad Bridge$190MMerkle root initialization flawBridge
2023Euler Finance$197MDonation attack + liquidation logicFlash loan
2023Multichain$126MCentralized key managementInfrastructure

Case Study: The DAO (2016)

The attack that led to the Ethereum/Ethereum Classic hard fork.

Case Study: Wormhole Bridge (2022)

solidity
// Simplified vulnerability — signature verification bypass
// The guardian set verification could be bypassed because
// the 'verify_signatures' instruction accepted a fabricated
// SignatureSet account

// Attacker created a fake SignatureSet with valid-looking data
// bypassing the verification that messages came from guardians

// Impact: Attacker minted 120,000 wETH ($326M) on Solana
// without depositing any ETH on Ethereum

Oracle Manipulation

Price oracles feed external data (prices, randomness) to smart contracts. Manipulating an oracle lets attackers trick protocols into using incorrect prices.

Oracle TypeRisk LevelExample
Spot price (single DEX)CriticalUniswap getReserves() — trivially manipulable
TWAP (Time-Weighted Average)MediumUniswap V3 TWAP — harder but not impossible
Chainlink feedsLowDecentralized oracle network — multiple data sources
Band ProtocolLowCross-chain oracle with economic incentives

Never Use Spot Prices as Oracles

Using getReserves() or similar single-block price sources is the most common oracle vulnerability. Always use time-weighted averages (TWAP) or decentralized oracles like Chainlink.


Bridge Security

Cross-chain bridges are the highest-value targets in Web3. They hold locked assets on one chain while minting wrapped assets on another.

Bridge ArchitectureHow It WorksRisk
Lock-and-MintLock on Chain A, mint on Chain BIf mint verification fails, unlimited minting
Burn-and-MintBurn on Chain A, mint on Chain BIf burn is faked, unlimited minting
Liquidity PoolSwap between pools on each chainIf pool is drained, insolvency
Validator SetN-of-M validators attest to transactionsIf M/2+1 validators compromised, game over

Smart Contract Auditing Methodology

Audit Process

Audit Checklist

CategoryCheckSeverity if Missing
ReentrancyAll external calls follow CEI patternCritical
Access ControlAll admin functions have proper modifiersCritical
Input ValidationAll inputs validated (zero address, bounds)High
Oracle SecurityPrice feeds use TWAP or Chainlink, not spotCritical
Flash LoanProtocol logic safe against atomic composabilityCritical
Integer SafetySolidity 0.8+ or SafeMath usedHigh
Front-RunningCommit-reveal or private mempools for sensitive opsMedium
CentralizationMulti-sig for admin, timelock for upgradesHigh
Upgrade SafetyStorage layout preserved across upgradesCritical
Gas GriefingNo unbounded loops, no external call in loopsMedium

Security Tools

Static Analysis

bash
# Slither — static analysis framework by Trail of Bits
pip install slither-analyzer
slither contracts/ --print human-summary
slither contracts/ --detect reentrancy-eth,reentrancy-no-eth
slither contracts/ --print contract-summary

# Mythril — symbolic execution
pip install mythril
myth analyze contracts/Vault.sol --solv 0.8.20
myth analyze contracts/Vault.sol --execution-timeout 300

# Aderyn — Rust-based Solidity analyzer
aderyn contracts/

Fuzzing

bash
# Foundry fuzzing — property-based testing
# Write invariant tests in Solidity

# Example fuzz test (Foundry)
# forge test --match-test testFuzz
solidity
// Foundry fuzz test example
contract VaultFuzzTest is Test {
    Vault vault;

    function setUp() public {
        vault = new Vault();
    }

    // Foundry automatically fuzzes the 'amount' parameter
    function testFuzz_DepositWithdraw(uint256 amount) public {
        vm.assume(amount > 0 && amount < 100 ether);

        vault.deposit{value: amount}();
        assertEq(vault.balances(address(this)), amount);

        vault.withdraw();
        assertEq(vault.balances(address(this)), 0);
    }

    // Invariant test — total balance should always match contract ETH
    function invariant_solvency() public {
        assertEq(
            address(vault).balance,
            vault.totalDeposits()
        );
    }
}
bash
# Run Foundry tests
forge test -vvv
forge test --match-test testFuzz -vvv

# Run invariant tests
forge test --match-test invariant -vvv

# Echidna — Haskell-based smart contract fuzzer
echidna contracts/Vault.sol --contract VaultEchidnaTest --config echidna.yaml

Formal Verification

ToolApproachBest For
Certora ProverSMT-based formal verificationDeFi protocols, critical invariants
HalmosSymbolic testing for FoundryProperty verification
KEVMK Framework semantics for EVMLow-level bytecode verification

Web3 Security Best Practices

PracticeDescription
Use OpenZeppelinBattle-tested, audited contracts for common patterns
Multiple auditsNo single auditor catches everything; get 2-3 audits
Bug bounty programImmunefi, HackerOne — let whitehats find bugs post-deploy
Timelock on upgrades48-72h delay on admin actions gives users time to exit
Multi-sig adminNever a single EOA as admin; use Gnosis Safe (3/5 minimum)
MonitoringForta, OpenZeppelin Defender for real-time alerting
Circuit breakersPause functionality for emergency response
Gradual rolloutCap TVL initially, increase limits over time

Web3 Bug Bounty Platforms

PlatformFocusMax BountyPrograms
ImmunefiDeFi, smart contracts$10M+ (some protocols)300+
HackerOneGeneral + Web3VariesGrowing
Code4renaCompetitive audits$100K+ per contestRegular contests
SherlockAudit contests + coverage$50K+ per contestGrowing

Getting Into Web3 Security

  1. Learn Solidity fundamentals (CryptoZombies, Solidity by Example)
  2. Study past exploits (Rekt.news, DeFi Hack Labs)
  3. Practice: Ethernaut, Damn Vulnerable DeFi, Capture the Ether
  4. Use Foundry for testing and exploitation
  5. Start with Code4rena or Sherlock contests

Further Reading


Key Takeaway

  • Smart contracts are immutable — a vulnerability discovered post-deployment is permanently exploitable unless an upgrade mechanism was built in
  • Reentrancy remains the most devastating vulnerability: always follow the Checks-Effects-Interactions pattern and use OpenZeppelin's ReentrancyGuard
  • Flash loans enable atomic composability attacks — any protocol using spot prices as oracles is vulnerable to price manipulation in a single transaction
Hands-On Lab

Lab: Exploit Vulnerable Smart Contracts

  1. Install Foundry (curl -L https://foundry.paradigm.xyz | bash && foundryup)
  2. Complete the first 5 levels of Ethernaut (OpenZeppelin's Solidity CTF)
  3. Set up a local Ethereum environment with Anvil (anvil)
  4. Write a reentrancy exploit: deploy the VulnerableVault contract from this page, then deploy the Attacker contract and drain it
  5. Fix the vulnerability using the Checks-Effects-Interactions pattern
  6. Run Slither static analysis on both the vulnerable and fixed versions — verify Slither catches the reentrancy
  7. Write a Foundry fuzz test that verifies the solvency invariant (contract balance always equals total deposits)
CTF Challenge

Challenge: The Broken Vault

A DeFi vault contract allows users to deposit and withdraw ETH. The contract has a withdraw() function that sends ETH before updating the user's balance. There is also a flashLoan() function that lends ETH without collateral. The vault holds 100 ETH. Drain it.

Hints:

  1. The withdraw() function is vulnerable to reentrancy
  2. You need to deposit some ETH first to have a non-zero balance
  3. Your attacker contract's receive() function should re-enter withdraw()
Answer

Deploy an attacker contract that deposits 1 ETH, then calls withdraw(). In the attacker's receive() function, check if the vault balance is greater than 1 ETH and re-enter withdraw(). The loop drains 100 ETH because the balance is only set to zero after the external call. Fix: move balances[msg.sender] = 0 before the external call. Flag: CTF{checks_effects_interactions_saves_billions}.

:::

Common Misconceptions

  • "Solidity 0.8 prevents all math bugs" — Solidity 0.8 adds overflow checks by default, but unchecked blocks bypass them. Rounding errors, precision loss, and logic errors in calculations remain common.
  • "An audit means the contract is safe" — No single audit catches everything. Major exploits (Wormhole, Euler) occurred in audited contracts. Multiple independent audits, formal verification, and bug bounties provide layered assurance.
  • "Decentralized means no single point of failure" — Many DeFi protocols have admin keys, upgradeability, or oracle dependencies that are centralized. A compromised admin key is game over.
  • "Using OpenZeppelin means your contract is secure" — OpenZeppelin provides secure building blocks, but your custom logic that connects them can still be vulnerable. The most exploited code is almost always the custom parts.
Quiz

1. What is the Checks-Effects-Interactions pattern?

a) A design pattern for UI components b) Validate conditions first, update state second, make external calls last c) A method of testing smart contracts d) An encryption algorithm

Answer

b) Checks-Effects-Interactions ensures all state changes (Effects) happen before any external calls (Interactions), preventing reentrancy attacks where external calls re-enter the function before state is updated.

2. Why are flash loans dangerous for DeFi protocols?

a) They charge high interest rates b) They allow atomic manipulation of prices and state within a single transaction c) They require collateral d) They are illegal

Answer

b) Flash loans provide uncollateralized capital that must be repaid within the same transaction. This enables attackers to manipulate oracle prices, exploit arbitrage, and drain protocols — all atomically with zero risk.

3. What makes bridge protocols the highest-value targets in Web3?

a) They are the oldest protocols b) They hold locked assets from multiple chains, making them high-value honeypots c) They use the simplest code d) They have no security audits

Answer

b) Bridges lock assets on one chain and mint wrapped assets on another. They aggregate value from multiple chains, making them the most lucrative targets — several bridges have lost hundreds of millions.

4. What tool performs static analysis on Solidity contracts?

a) Nmap b) Slither c) Burp Suite d) Wireshark

Answer

b) Slither (by Trail of Bits) is the primary static analysis tool for Solidity. It detects reentrancy, access control issues, and other vulnerability patterns without executing the code.

5. Why should you never use a single DEX spot price as a price oracle?

a) DEX prices are always wrong b) Spot prices can be manipulated within a single transaction using flash loans c) Spot prices are too slow d) DEX prices require API keys

Answer

b) A flash loan can temporarily manipulate a DEX's spot price by executing a large swap, interacting with the victim protocol at the manipulated price, then reversing the swap — all in one transaction.

:::

One-Liner Summary: In Web3, code is law and bugs are permanent — a single smart contract vulnerability can drain billions in a single transaction.

"What I cannot create, I do not understand." — Richard Feynman