Skip to main content
Ethereum & smart contracts

Common Smart Contract Bugs

Pomegra Learn

Common Smart Contract Bugs

Smart contracts are permanent programs executing on immutable blockchains. Bugs aren't simply inconveniences—they're often exploited to steal funds or disrupt services. This chapter covers the most critical vulnerabilities, explaining how they occur and how developers prevent them.

Reentrancy Attacks

Reentrancy is among the most dangerous smart contract vulnerabilities. It occurs when a contract calls another contract and the called contract unexpectedly calls back into the original contract before the initial function completes.

How Reentrancy Works

Consider a contract allowing users to withdraw ETH. The withdrawal function performs three steps:

  1. Check user balance
  2. Send ETH to user
  3. Reduce user balance

If step 2 (sending ETH) triggers the recipient contract's fallback function, that fallback could call the withdrawal function again. When the recursive call executes, step 1 (checking balance) succeeds because step 3 hasn't completed yet. The contract pays out the same balance multiple times.

The famous DAO hack in 2016 exploited reentrancy, resulting in 50 million dollars in stolen funds. The vulnerability was subtle but catastrophic.

The Fix: Checks-Effects-Interactions

Developers prevent reentrancy by reordering operations. The safe pattern is:

  1. Check preconditions
  2. Update state (effects)
  3. Make external calls (interactions)

By reducing balance before sending ETH, reentrancy becomes impossible. If the recursive call attempts withdrawal again, the balance check fails.

Modern development practices implement this pattern consistently. Many developers use the ReentrancyGuard contract from OpenZeppelin, which uses mutex-style locking to prevent concurrent execution of protected functions.

Integer Overflow and Underflow

Before Solidity 0.8.0, arithmetic operations could overflow or underflow without warnings. If a uint256 exceeds its maximum value (2^256 - 1), it wraps to zero. Underflow similarly wraps large numbers.

These bugs created exploitable conditions. A contract might track balances using unsigned integers. Through manipulation, attackers could cause balances to underflow, giving themselves unlimited funds.

Modern Solidity automatically reverts on overflow/underflow, eliminating this class of bugs. Developers using older Solidity versions must use SafeMath libraries to prevent arithmetic errors. Modern code rarely faces this vulnerability, but legacy contracts sometimes still contain exploitable overflow conditions.

Access Control Vulnerabilities

Smart contracts often have privileged operations—changing settings, transferring funds, or modifying contracts. Insufficient access control allows unauthorized users to perform sensitive operations.

Common Patterns

Missing checks: Functions allowing fund transfers might omit verification that the caller is authorized. Anyone could call the function and steal funds.

Incorrect permissions: Developers might check permissions but compare against the wrong address or variable. Subtle logic errors create security gaps.

Public vs. External: In older Solidity, mistakenly using public instead of internal for sensitive functions exposed them to unauthorized callers.

Delegatecall vulnerabilities: Some contracts use delegatecall to execute external code in their own context. If delegatecall is used carelessly, attackers can call arbitrary code with the contract's privileges.

Prevention

Modern contracts use established patterns like OpenZeppelin's Ownable (single owner) and AccessControl (role-based permissions). These libraries handle authorization consistently and securely.

Contracts should have clear documentation showing which functions are privileged and what permissions are required.

Logic Bugs and Design Flaws

Beyond obvious vulnerabilities, many exploits result from subtle logic errors.

Example: Price Oracle Attacks

A lending protocol might check collateral value using a price oracle—a service reporting asset prices. If the oracle is manipulated (through flash loans or other attacks), the contract makes incorrect decisions. An attacker might deposit worthless collateral, borrow valuable assets, and keep the collateral.

This requires understanding the contract's interaction with external systems, not just the contract code itself.

Example: Front-Running

On public blockchains, all pending transactions are visible. An attacker seeing a profitable transaction in the mempool can submit their own transaction first (with higher gas), executing before the original transaction. This "front-runs" the original transaction, enabling theft or manipulation.

DEX trades are common front-running targets. An attacker seeing a large pending swap can swap first, moving the price against the original trader, then unwind their position for profit.

Flash Loan Attacks

Flash loans are uncollateralized loans that must be repaid within the same transaction. They're extremely powerful tools for sophisticated attacks.

How Flash Loans Enable Attacks

A flash loan allows borrowing millions of dollars without collateral, provided it's repaid in the same transaction. An attacker borrows a massive amount, executes a series of transactions manipulating prices or exploiting logic bugs, then repays the loan plus a small fee.

Many exploit vectors depend on temporarily inflated balances or price movements. Flash loans amplify these possibilities.

Mitigation

Contracts should be skeptical of instantaneous state changes. Price oracles shouldn't update dramatically within single transactions. Lending protocols should carefully consider how flash loans interact with collateral valuation.

Many recent vulnerabilities involve flash loan abuse, making flash loan awareness essential for modern protocol designers.

Selfdestruct and Unwanted ETH

The selfdestruct opcode allows contracts to destroy themselves, sending all ETH to a specified address. This can cause unexpected behavior if contracts depend on their balance being zero.

An attacker can forcibly send ETH to any address using selfdestruct. A contract expecting zero ETH balance might malfunction if forcibly given ETH.

This vulnerability is rare but dangerous. Secure code assumes addresses might receive ETH unexpectedly.

Timestamp Dependence

Some contracts use block timestamps for important decisions (lotteries, time-locked operations, etc.). Miners can manipulate timestamps within constraints, creating predictability opportunities.

Modern contracts should avoid timestamp-dependent randomness or time-locks that are too short. Better approaches use block numbers (harder to manipulate) or external randomness sources.

Gas Limit Issues

Early contract exploits involved running out of gas during execution, causing partial state changes. Modern EVM improvements reduced these risks, but gas limits still matter.

Very large arrays or loops can consume more gas than expected, causing transactions to fail. Contracts should be aware of computational complexity and avoid unbounded loops.

Delegatecall and Context Confusion

delegatecall executes code in the caller's context, using the caller's storage and msg.sender. This is powerful for proxies and libraries but dangerous if misused.

If a contract uses delegatecall on untrusted addresses, attackers can execute arbitrary code with the contract's storage and permissions. This is catastrophic.

The Parity wallet suffered a delegatecall vulnerability that allowed attackers to freeze millions of dollars in frozen accounts.

Division by Zero

Smart contracts performing division should check denominators aren't zero. Division by zero reverts in modern Solidity, but logic should assume this error won't occur.

More sophisticated issues involve denominators becoming zero due to prior state changes. Careful ordering of operations prevents this.

Missing Input Validation

Functions should validate all inputs. Missing checks allow unexpected values to propagate, potentially causing exploitable behavior.

Examples include:

  • Array index out of bounds
  • Recipient addresses being zero (burning funds unintentionally)
  • Invalid token addresses causing failed transfers
  • Amounts exceeding allowed ranges

Comprehensive input validation is standard security practice.

Block Dependency Randomness

Some contracts use block hash for randomness. Miners can influence block hashes through transaction ordering, making this predictable randomness.

Proper randomness sources include Chainlink VRF, which provides cryptographically secure randomness. Using native blockchain data for randomness should be avoided.

Prevention and Auditing

Most high-severity bugs are preventable through:

Code review: Experienced developers spotting common patterns and vulnerabilities.

Automated analysis: Tools like Slither automatically detect many vulnerability patterns.

Professional audits: Security firms systematically review contracts for vulnerabilities.

Testing: Comprehensive test coverage catches many edge cases during development.

Formal verification: Mathematically proving contract properties prevents entire classes of bugs.

The combination of careful development practices, automated tooling, and professional review significantly reduces vulnerability risk.

Real-World Examples

The history of smart contract exploits provides valuable lessons:

The 2016 DAO hack exploited reentrancy, stealing 50 million dollars and causing a controversial hard fork.

Parity's wallet vulnerability in 2017 froze hundreds of millions in ETH through a delegatecall bug.

2020 flash loan attacks exploited DeFi protocols through price oracle manipulation and logic errors.

2022 Ronin Bridge exploit compromised validator security, not contract code, but demonstrated attack diversity.

Each incident spurred improvements in auditing practices and developer awareness.

The Security Ecosystem

Modern smart contract development has mature security practices:

OpenZeppelin Contracts: Audited, battle-tested libraries providing safe implementations of common patterns.

Auditing firms: Specialized companies providing thorough security reviews before deployment.

Bug bounties: Projects offer rewards for responsible vulnerability disclosure.

Formal verification tools: Proving mathematical properties of contracts before deployment.

Monitoring services: Watching contracts for suspicious activity post-deployment.

While security is never absolute, the combination of tools, practices, and expertise available today dramatically reduces serious vulnerabilities in production contracts.

Future Improvements

The security landscape continues evolving:

Domain-specific languages: Languages designed for blockchain might prevent entire vulnerability classes.

Formal verification at scale: Making mathematical proofs practical for complex systems.

Runtime monitoring: Detecting exploits and pausing operations before damage occurs.

Decentralized security: Community-run security services and distributed auditing.

Smart contract security will never be perfect, but continuous improvement in tools and practices raises the bar for potential attackers.

Conclusion

Smart contract bugs range from obvious logic errors to subtle interactions between components. The stakes are high—bugs result in stolen funds and disrupted services. The bright side is that most vulnerabilities are preventable through established practices: careful development, comprehensive testing, professional auditing, and community review.

Any significant smart contract should undergo professional security review before deployment. Users should never interact with unaudited contracts managing substantial funds. The combination of verified code, professional audits, and thoughtful design creates secure, reliable smart contracts.


Related Articles:

External Sources: