All articles

Smart Contract Security: Common Vulnerabilities and How to Prevent Them

The Solidity bugs behind most smart contract losses, from reentrancy and missing access control to price oracle manipulation and signature replay, plus the testing tools and audit process that catch them before deployment.

Blockchain|Published |11 min read
3D render of connected blocks forming a chain

A bug in a web application can be patched on Friday afternoon. A bug in a deployed smart contract is public, permanent unless you planned for upgrades, and often sitting next to a pile of tokens anyone can take. Most large losses do not come from exotic cryptography. They come from a small set of well known mistakes. Learning to spot them is the most valuable thing a Solidity developer can do before writing contracts that hold real value.

Reentrancy

When a contract sends ETH or calls another contract, the receiver gets control and can call back into the original contract before the first call finishes. If the balance is updated after the transfer, the attacker withdraws again and again from the same balance. This is the bug behind the 2016 DAO hack, and variants of it still appear, including read-only reentrancy where another protocol reads a stale value in the middle of the call.

  • Follow checks, effects, interactions: validate inputs, update state, and only then make external calls.
  • Add OpenZeppelin ReentrancyGuard to functions that move funds.
  • Prefer letting users withdraw their own funds over pushing payments to many addresses in one function.

Missing or Broken Access Control

A mint, pause, or upgrade function without an onlyOwner or role check can be called by anyone. Upgradeable contracts add a quieter version of the same bug: an initializer that was never called or never locked, which lets an attacker initialize the implementation and make themselves owner. Use OpenZeppelin Ownable or AccessControl, call _disableInitializers in the implementation constructor, and write a test for every privileged function that calls it from a random address and expects a revert.

Never authorize with tx.origin. It is the address that started the transaction, so a malicious contract the owner interacts with can pass the check. Use msg.sender.

Price Oracle Manipulation

A lending protocol that reads an asset price from the current reserves of a single DEX pool can be fooled within one transaction. The attacker borrows a large amount with a flash loan, moves the pool price, borrows against the inflated collateral, and repays the flash loan, all in the same block. Use a decentralized oracle such as Chainlink, or a time-weighted average price over a sensible window, and check that the price is fresh and within expected bounds.

Other Bugs Worth Knowing

VulnerabilityWhat goes wrongPrevention
Signature replayA signed message is reused on another chain, another contract, or a second timeUse EIP-712 typed data with a nonce, chain ID, contract address, and deadline
Unchecked low-level callcall returns false on failure, and the code continues as if the transfer workedCheck the return value, and use SafeERC20 for tokens that do not return a boolean
Front-runningOthers see the transaction in the mempool and act first, for example on a swapSlippage limits, deadlines, and commit-reveal schemes where order matters
Unbounded loopsA loop over a growing array eventually needs more gas than a block allows, locking the functionPaginate the work or let each user process their own entry
Storage collision in upgradesA new implementation reorders state variables and corrupts existing dataOnly append variables, and run the OpenZeppelin upgrades plugin to validate layouts
Arithmetic in unchecked blocksOverflow checks are skipped where a developer assumed the numbers could not overflowUse unchecked only where the bound is proven, and keep precision by multiplying before dividing

Solidity 0.8 and later revert on integer overflow by default, which removed a whole class of older bugs. Rounding is still a problem, especially in vaults and share calculations, where rounding in the user's favour can be exploited many times over.

Tools That Catch Bugs Early

  • Slither, a static analyzer that flags reentrancy, unprotected functions, shadowed variables, and many other patterns in seconds. Run it in CI.
  • Foundry fuzz tests, which call your functions with thousands of random inputs and find edge cases you would not write by hand.
  • Invariant tests in Foundry or Echidna, which check properties that must always hold, such as total deposits equalling the sum of all balances.
  • Mainnet fork tests, which run your contract against real deployed protocols and real prices.
  • Battle-tested libraries such as OpenZeppelin Contracts for tokens, access control, and proxies, instead of writing your own.

A Release Process for Contracts That Hold Value

  1. 1Write a short specification of what each function may and may not do, and the invariants of the system.
  2. 2Reach high test coverage, including fuzz and invariant tests, before anyone outside the team reads the code.
  3. 3Run Slither and fix or document every finding.
  4. 4Freeze the code and get an external audit. Two independent audits are common for contracts holding significant funds.
  5. 5Deploy to a testnet such as Sepolia and run the full user flow, including admin actions.
  6. 6Put admin keys in a multisig wallet and add a timelock to upgrades, so users can see changes before they take effect.
  7. 7Launch with a bug bounty and monitoring on large transfers and privileged calls.

An audit is a second pair of eyes on code that should already be well tested. It does not replace the tests.

Our smart contract training covers these vulnerabilities by having participants exploit deliberately weak contracts first, then fix and test them with Foundry. Seeing an attack succeed makes the defensive habits stick much better than reading a checklist.

Key takeaways

  • Update state before external calls, and add ReentrancyGuard to functions that move funds.
  • Protect every privileged function with roles, lock initializers, and never authorize with tx.origin.
  • Do not read prices from a single pool. Use Chainlink or a time-weighted price and check freshness.
  • Use EIP-712 signatures with nonce, chain ID, and deadline to stop replay.
  • Combine Slither, fuzz and invariant tests, an external audit, a multisig, and a bug bounty before holding real value.

Related articles

More articles on software development, AI, cloud, and infrastructure.

Hands sorting printed tax forms and receipts next to a calculator
AI Development|

Automating Document Data Entry With AI: Invoices, Receipts, and Forms

How AI reads invoices, receipts, ID cards, and forms and turns them into structured data, how OCR and large language models fit together, why validation and human review still matter, how to measure accuracy, and what to watch for with personal data.

Aerial view of a busy container port with cranes and stacked shipping containers
DevOps|

Deploying Applications to Kubernetes With Helm: A Practical Guide

What Helm charts are, how values and templates work, how to structure a chart for several environments, upgrade and rollback safely, keep secrets out of Git, test charts in CI, and decide between Helm and Kustomize.

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation