Part 5.4 of 6

Smart Contract Vulnerabilities

120 minutes
Advanced Level

Introduction to Smart Contract Security

Smart contracts are self-executing programs deployed on blockchain networks that automatically enforce the terms of agreements without intermediaries. While this automation eliminates counterparty risk in many scenarios, it introduces a new class of risk: code vulnerabilities that can result in immediate, irreversible loss of funds. Unlike traditional software where bugs can be patched, smart contracts are typically immutable once deployed, making security a paramount concern.

The unique properties of smart contracts amplify the impact of vulnerabilities. Code is publicly visible, allowing attackers to identify and exploit weaknesses. Transactions are irreversible, meaning successful exploits cannot be undone. And the direct link between code execution and value transfer means that vulnerabilities translate immediately to financial loss. Since the emergence of DeFi in 2020, over $7 billion has been lost to smart contract exploits.

Smart Contract Immutability

Once deployed to a blockchain, smart contract code typically cannot be changed. This immutability is a feature, providing certainty that contract logic will execute as written. However, it means that vulnerabilities cannot be patched in place. Upgradeable contract patterns exist but introduce their own security considerations and trust assumptions.

Understanding smart contract vulnerabilities is essential for blockchain security professionals, whether conducting security audits, investigating exploits, advising on protocol safety, or assessing investment risk. This module covers the most common vulnerability classes, real-world exploitation case studies, and the security practices that help prevent these failures.

The Ethereum Virtual Machine (EVM)

Most smart contracts today run on the Ethereum Virtual Machine (EVM), a stack-based execution environment implemented by Ethereum and compatible blockchains (Binance Smart Chain, Polygon, Arbitrum, etc.). Understanding EVM mechanics is essential for comprehending many vulnerability classes.

  • Solidity: The dominant programming language for EVM smart contracts. Solidity's syntax resembles JavaScript but has unique semantics that can lead to unexpected behavior.
  • Gas: Every EVM operation costs "gas," paid by transaction initiators. Gas limits can cause transactions to fail partway through execution, potentially leaving contracts in inconsistent states.
  • Storage and memory: Contracts have persistent storage on the blockchain and temporary memory during execution. Storage is expensive (gas-wise) and its layout can introduce vulnerabilities.
  • External calls: Contracts can call other contracts, enabling composability but also creating attack vectors when untrusted code is invoked.

Common Vulnerability Classes

Smart contract vulnerabilities can be categorized into several major classes. Understanding these patterns enables security professionals to identify risks during code review and to understand exploits after they occur.

Reentrancy
Critical

Occurs when a contract makes an external call before updating its state, allowing the called contract to re-enter the original function and execute it again with stale state. The DAO hack ($60M) exploited reentrancy.

Integer Overflow/Underflow
High

Arithmetic operations that exceed data type limits can wrap around, producing unexpected values. A subtraction from zero can yield the maximum uint256 value. Solidity 0.8+ includes automatic overflow checks, but legacy contracts remain vulnerable.

Access Control
Critical

Functions that should be restricted to specific roles (owner, admin, authorized contracts) but are callable by anyone. Missing or incorrect modifiers can allow unauthorized users to drain funds, pause contracts, or change critical parameters.

Flash Loan Attacks
Critical

Flash loans provide unlimited uncollateralized borrowing within a single transaction. Attackers use flash loans to temporarily acquire massive capital for market manipulation, oracle attacks, and governance exploits. Defenses require careful economic modeling.

Oracle Manipulation
High

Protocols that rely on manipulable price sources (spot prices, low-liquidity pairs) can be exploited when attackers temporarily distort prices. Proper oracle design using TWAPs or decentralized oracles (Chainlink) is essential but not foolproof.

Front-Running
Medium

Transactions in the mempool are visible before inclusion in blocks. Attackers can observe pending transactions and submit competing transactions with higher gas fees to execute first, extracting value through arbitrage, sandwich attacks, or transaction reordering.

Reentrancy Attacks in Depth

Reentrancy remains one of the most devastating vulnerability classes in smart contract security. It occurs when a contract makes an external call to an untrusted contract before completing its internal state updates. The external contract can then call back into the original contract, which still has its old state, allowing repeated execution of operations that should only occur once.

The Classic Pattern

Consider a simple withdrawal function that sends Ether to users:

// VULNERABLE CODE - DO NOT USE function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0, "No balance"); // VULNERABILITY: External call before state update (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); // State update happens AFTER the external call balances[msg.sender] = 0; }

An attacker's contract can implement a receive() or fallback() function that calls withdraw() again when it receives Ether. Since the balance hasn't been set to zero yet, the check passes and funds are sent again, recursively draining the contract.

The DAO Hack (June 2016)
$60 Million Stolen via Reentrancy

The DAO was an early decentralized investment fund that raised $150 million in ETH. An attacker exploited a reentrancy vulnerability in the "splitDAO" function to drain approximately $60 million (3.6 million ETH) before the attack was detected.

The vulnerability existed because the DAO sent ETH to users before updating their balance. The attacker's malicious contract recursively called splitDAO from its fallback function, draining funds with each iteration.

Aftermath: The Ethereum community controversially decided to hard fork the blockchain to restore the stolen funds, creating Ethereum (ETH) and Ethereum Classic (ETC). This event fundamentally shaped blockchain governance discussions and established reentrancy as a critical vulnerability class requiring careful attention.

Reentrancy Prevention Patterns

Checks-Effects-Interactions Pattern

The most important defensive pattern: perform all checks first, then update state (effects), and only make external calls (interactions) last. This ensures state is consistent before any external code executes.

// SAFE: Checks-Effects-Interactions pattern function withdraw() external { // CHECKS uint256 amount = balances[msg.sender]; require(amount > 0, "No balance"); // EFFECTS: Update state BEFORE external call balances[msg.sender] = 0; // INTERACTIONS: External call comes last (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); }
Reentrancy Guards (Mutex)

A reentrancy guard uses a state variable to prevent reentrant calls. OpenZeppelin's ReentrancyGuard is the standard implementation. The modifier sets a flag when the function is entered and reverts if the flag is already set, preventing recursive calls.

// Using OpenZeppelin ReentrancyGuard import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureVault is ReentrancyGuard { function withdraw() external nonReentrant { // Even with external calls, reentrancy is blocked uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success); } }

Cross-Function and Cross-Contract Reentrancy

Advanced reentrancy variants can occur across multiple functions or even multiple contracts that share state. Cross-function reentrancy exploits situations where one function modifies state that another function reads, and the attacker reenters through the second function. Cross-contract reentrancy occurs in multi-contract systems where contracts share state or trust each other.

Read-Only Reentrancy

A newer variant where the reentrant call reads state that will be updated after the external call completes. While not directly draining funds, this can be used to manipulate view functions that other protocols rely on, causing indirect losses in composable DeFi systems.

Access Control Vulnerabilities

Access control vulnerabilities occur when functions that should be restricted are accessible to unauthorized users. In smart contracts, this often means anyone can call functions meant only for administrators, owners, or other privileged roles. These vulnerabilities can be catastrophic, allowing attackers to drain funds, pause protocols, or take over contract ownership.

Missing Access Modifiers

The most common access control vulnerability is simply forgetting to add authorization checks. In Solidity, functions are public by default in older versions, meaning they can be called by anyone unless explicitly restricted.

// VULNERABLE: No access control on sensitive function function withdrawAll(address recipient) external { // Anyone can call this and drain all funds! payable(recipient).transfer(address(this).balance); } // SAFE: Only owner can withdraw function withdrawAll(address recipient) external onlyOwner { payable(recipient).transfer(address(this).balance); }

Improper Visibility

Solidity provides four visibility levels for functions: public, external, internal, and private. Incorrect visibility settings can expose internal functions or unnecessarily restrict external access.

Visibility Accessible From Use Case
public Anywhere (internal, external, inherited) API functions intended for all callers
external Only external calls (other contracts, EOAs) Functions not called internally (gas optimization)
internal This contract and inheriting contracts Helper functions for inheritance hierarchies
private Only this contract Implementation details not for inheritance

Authorization Logic Flaws

tx.origin vs msg.sender

Using tx.origin for authorization is dangerous because it returns the original external account that initiated the transaction, not the immediate caller. If an authorized user is tricked into calling a malicious contract, that contract can then call the vulnerable contract with the user's tx.origin. Always use msg.sender for authorization.

// VULNERABLE: tx.origin can be exploited function transferOwnership(address newOwner) external { require(tx.origin == owner); // WRONG! owner = newOwner; } // SAFE: msg.sender is the immediate caller function transferOwnership(address newOwner) external { require(msg.sender == owner); // Correct owner = newOwner; }

Unprotected Initializers

Upgradeable contracts using proxy patterns typically have an initialize() function instead of a constructor. If this function is not properly protected, attackers can call it to take ownership of the implementation contract or reinitialize proxies with malicious parameters.

Wormhole Bridge Hack (February 2022)
$320 Million Lost to Unprotected Initializer

The Wormhole cross-chain bridge lost $320 million when an attacker exploited an unprotected initializer in a Solana smart contract. The vulnerability allowed the attacker to create fake guardian signatures that appeared valid to the bridge, enabling them to mint 120,000 wrapped ETH without depositing collateral.

Key Lesson: Initialization functions must be protected from being called after initial deployment. OpenZeppelin's Initializable pattern provides an initializer modifier that ensures the function can only be called once.

Economic Exploits

Economic exploits represent a category of attacks that manipulate economic mechanisms rather than exploiting code bugs. These attacks leverage flash loans, price oracles, and market dynamics to extract value from DeFi protocols. They are particularly challenging to defend against because the underlying code may function exactly as designed.

Flash Loan Attacks

Flash loans provide instant, uncollateralized borrowing as long as the loan is repaid within the same transaction. This enables attackers to temporarily wield millions in capital to manipulate prices, exploit arbitrage opportunities, or amplify other vulnerabilities.

Flash Loan Attack Pattern

1. Borrow large amount via flash loan (e.g., $10 million USDC)
2. Use borrowed funds to manipulate a price or exploit a vulnerability
3. Extract profit from the manipulation
4. Repay flash loan plus fee
5. Keep the profit (often millions of dollars)

Flash loans enable attacks that would otherwise require substantial capital. An attacker with $1,000 can execute the same manipulation as someone with $100 million, as long as the attack is profitable within a single transaction.

Oracle Manipulation

Many DeFi protocols rely on price oracles to determine asset values for lending, liquidations, and derivatives. If these oracles can be manipulated, attackers can cause protocols to make decisions based on incorrect prices.

Spot Price Oracles
Critical Risk

Using spot prices from DEX pools as oracles is dangerous because these prices can be manipulated within a single transaction using flash loans. An attacker can temporarily distort the pool ratio, take an action based on the wrong price, then restore the pool.

Low-Liquidity Token Pricing
High Risk

Tokens with low liquidity can have their prices easily moved with relatively small trades. Protocols that accept such tokens as collateral must carefully consider manipulation risks and may need to apply discounts or restrictions.

Oracle Security Best Practices

  • Use decentralized oracles: Chainlink and similar services aggregate prices from multiple sources with cryptographic guarantees, making manipulation much more difficult.
  • Time-weighted average prices (TWAPs): Rather than using spot prices, calculate averages over time windows. Manipulating TWAPs requires sustained capital commitment.
  • Multiple oracle sources: Compare prices from multiple oracles and reject outliers. If sources disagree significantly, pause operations or use fallback mechanisms.
  • Price deviation limits: Reject prices that deviate too far from recent values. This prevents single-block manipulation but may cause issues during legitimate volatility.
  • Collateral quality assessment: Apply conservative loan-to-value ratios and liquidity discounts to assets that are easier to manipulate.
Mango Markets Exploit (October 2022)
$117 Million Drained via Oracle Manipulation

Avraham Eisenberg publicly exploited Mango Markets, a Solana-based DeFi platform, by manipulating the price of the MNGO token. Using two accounts, he took opposing positions and then pumped the MNGO price, causing his long position to show massive unrealized profit.

He then borrowed against this inflated collateral value, draining $117 million from the platform. Eisenberg initially claimed this was a "highly profitable trading strategy" rather than an exploit, but was later arrested and charged with commodities fraud.

Key Lessons: This attack highlighted that economic exploits can be as damaging as code vulnerabilities. Protocols must carefully consider manipulation resistance in their economic design, including collateral quality, oracle selection, and position limits.

Security Audits and Testing

Security audits are systematic reviews of smart contract code to identify vulnerabilities before deployment. While audits are not guarantees of security, they represent a critical component of responsible smart contract development and are increasingly expected by users and regulators.

Audit Process Overview

  • Scope definition: Clearly define which contracts and functionality are in scope. Include dependencies and external integrations if they affect security.
  • Manual review: Experienced auditors examine code line-by-line, looking for known vulnerability patterns and logic errors. This remains the most effective method for finding complex bugs.
  • Automated analysis: Static analysis tools (Slither, Mythril, Securify) identify common vulnerability patterns. These complement but do not replace manual review.
  • Formal verification: Mathematical proof that code satisfies its specification. Powerful but expensive and only covers what's specified.
  • Report and remediation: Auditors document findings with severity ratings. Development teams fix issues and auditors verify remediations.

Audit Checklist

Access Control Verify all privileged functions have proper authorization. Check for missing modifiers.
Reentrancy Check all external calls. Verify Checks-Effects-Interactions pattern or reentrancy guards.
Arithmetic Check for overflow/underflow (pre-Solidity 0.8). Review division and rounding.
Oracle Security Verify oracle sources are manipulation-resistant. Check for flash loan attack vectors.
Input Validation Check all external inputs are validated. Look for zero address, empty arrays, extreme values.
Upgrade Safety Verify initializers are protected. Check storage layout compatibility for upgrades.

Limitations of Audits

Audits Are Not Guarantees

Many hacked protocols had audits, sometimes multiple audits from reputable firms. Audits are point-in-time assessments that may miss complex bugs, especially those involving economic mechanisms or multi-contract interactions. Audits should be part of a comprehensive security strategy that includes bug bounties, monitoring, and incident response planning.

Bug Bounty Programs

Bug bounty programs incentivize security researchers to find and responsibly disclose vulnerabilities. Well-funded bounties can attract skilled researchers who may find issues missed during audits. Platforms like Immunefi specialize in cryptocurrency bug bounties, with some programs offering millions of dollars for critical vulnerabilities.

  • Proportional rewards: Bounties should be proportional to potential impact. If a vulnerability could steal $100 million, a $10,000 bounty may not incentivize disclosure.
  • Clear scope and rules: Define what's in scope, acceptable testing methods, and reporting procedures to avoid confusion.
  • Responsive triage: Researchers expect timely acknowledgment and fair evaluation of their reports. Poor communication damages reputation.
  • Safe harbor: Provide legal protection for good-faith security research within program rules.

Key Takeaways

  • Smart contract vulnerabilities cause immediate, irreversible losses. Unlike traditional software, deployed smart contracts typically cannot be patched. Code is publicly visible, enabling attackers to identify and exploit weaknesses. Security must be built in from the start.

  • Reentrancy remains a critical vulnerability class. The Checks-Effects-Interactions pattern and reentrancy guards are essential defenses. Variants like cross-function and read-only reentrancy require careful attention in complex systems.

  • Access control failures enable catastrophic losses. Missing or incorrect authorization checks allow attackers to call privileged functions. Verify all sensitive functions have proper modifiers and never use tx.origin for authentication.

  • Economic attacks exploit design rather than code bugs. Flash loans enable capital-intensive manipulations. Oracle manipulation distorts protocol decision-making. Defenses require careful economic modeling, not just code review.

  • Audits are necessary but not sufficient. Many hacked protocols had audits. Audits are point-in-time assessments that complement but do not replace bug bounties, monitoring, formal verification, and comprehensive testing.

  • Use established patterns and libraries. OpenZeppelin and similar audited libraries provide tested implementations of common functionality. Reinventing security primitives introduces unnecessary risk.