The Most Common Smart Contract Vulnerabilities and How Developers Can Prevent Them
Thieves took over $3.4 billion in crypto between January and early December 2025, according to Chainalysis, and one incident, the February compromise of Bybit, accounted for $1.5 billion of it. That figure usually gets quoted as proof that attackers have become frighteningly sophisticated. I read it the other way. Most of that money left through a handful of old, well-documented failure classes: reentrancy, broken access control, prices that can be pushed around, unguarded initializers, and signers who approved something they couldn’t actually see. None of it is new. Every one of these classes has a public post-mortem older than most of the protocols it keeps draining.
I’ve written Solidity for protocols that held real deposits and reviewed far more of it than I’ve written. The pattern I keep running into has a name in my notes: old bugs, new money. The bug class is a decade old. The TVL sitting behind it is fresh, and so is the team that assumed the problem had been solved somewhere upstream. What follows is a set of case files, one per vulnerability class. Each opens with an exploit you can look up, then the mechanism in plain terms, then the patterns that actually stop it.
Reentrancy is ten years old and still getting paid
On June 17, 2016, an attacker started draining The DAO by calling back into its split logic before the contract had updated balances. The drain pulled over 3.6 million ETH out of an insecure contract, and the fix, a hard fork at block 1,920,000 on July 20, split the chain into Ethereum and Ethereum Classic for good. Seven years later, on July 30, 2023, Curve pools for alETH, msETH, pETH and CRV/ETH lost $69.3 million to the same class of bug. The difference was where it lived. Curve’s code applied the nonreentrant decorator correctly. Vyper versions 0.2.15, 0.2.16, and 0.3.0 compiled that lock incorrectly, so it didn’t hold across functions.
In plain terms, the bug looks like this. Your contract hands control to an outside address (an ETH transfer, a token with transfer hooks, a callback) before it finishes updating its own state. The outside address calls back in and finds the old balances still sitting there. Single-function reentrancy is the textbook version, and few teams ship it now. Cross-function and read-only reentrancy are the versions that still ship, because the guard sits on one function while another function, sometimes inside a different protocol that integrates yours, reads the stale state.
Prevention: follow checks-effects-interactions with no exceptions. Validate inputs, write state, and only then make the external call. Put a reentrancy guard, such as OpenZeppelin’s ReentrancyGuard or its transient-storage version on chains that support EIP-1153, on every state-changing entry point, not only the ones that move ETH. Treat view functions that other protocols price against as part of your attack surface. And pin your compiler version on purpose, then read the known-bugs list for that exact version before you deploy. Curve’s developers did nothing wrong at the source level. That’s the uncomfortable part.
It’s also where outsourced builds go sideways most often. When founders ask me how to vet smart contract development services, my first test is boring on purpose: ask the team which compiler version they’d pin and why. A team with a real answer has read a post-mortem. A team that shrugs will be writing one.
Access control failures cost more than every other class combined
On August 10, 2021, Poly Network lost $611 million, the largest DeFi theft on record at the time. The attacker didn’t break any cryptography. They sent a cross-chain message that made Poly’s EthCrossChainManager contract call a privileged function on EthCrossChainData, the contract that stored the keeper public keys, and swapped those keys for their own. The manager owned the data contract and would execute whatever call a well-formed message asked for. Most of the money came back, which says more about the attacker than about Poly’s design.
The bug class is simple to state. A privileged function is reachable by someone who shouldn’t be able to reach it. Sometimes that’s a missing onlyOwner modifier. More often now it’s a trust chain: contract A may call contract B, and A will call anything an attacker can talk it into. Then there are keys. A single externally owned account holding upgrade rights over a nine-figure protocol is an access control bug even when every line of Solidity is correct.
The numbers back this up. The OWASP Smart Contract Top 10, which ranks categories using incident data, recorded $1.42 billion lost across 149 documented incidents in 2024. Access control vulnerabilities alone accounted for $953.2 million of that, more than every other category on the list combined. Reentrancy came in at $35.7 million and flash loan attacks at $33.8 million.
Prevention: replace the single owner with role-based access control, using OpenZeppelin’s AccessControl or AccessManager, and keep every role as narrow as the job it does. Put upgrades and parameter changes behind a multisig and a timelock, so a malicious proposal sits in public for a day or more before it can execute. Never give privileges to a contract that can forward arbitrary calldata. If a function relays calls, it should hold no roles at all. And test every privileged function from every unprivileged address. In Foundry that’s an afternoon.
An oracle that reads one thin market is a price anyone can buy
On October 11, 2022, Mango Markets on Solana lost $115 million to a trader who used two accounts to take both sides of a large MNGO perpetual position, then bought MNGO on the thin spot venues Mango’s oracle tracked. The reported price spiked, the long side showed a huge unrealized gain, and the attacker borrowed against it until the treasury was empty. He later called it a “highly profitable trading strategy.” The protocol did exactly what its code said.
This is price manipulation. The contract trusts a number that an attacker can move, briefly, at a cost lower than what the contract will pay out on the false number. Spot prices from a single AMM pool are the worst case.
Prevention: use time-weighted average prices over a window long enough that holding a manipulated price costs more than it earns, and pair the TWAP with an independent feed such as Chainlink or Pyth. Pause borrowing when the sources disagree. Cap how much an illiquid asset can count toward collateral. On L2s, check staleness and the sequencer uptime feed. Then model the attack in money, not code. What would it cost to push this price up five times for ten minutes, and what could someone borrow if they did? If the second number is bigger, you have a bug.
Flash loans don’t create bugs; they finance them
On March 13, 2023, Euler Finance lost $197 million. The attacker borrowed DAI through an Aave flash loan, built a levered position on Euler, then called donateToReserves, a function added in an earlier upgrade that let users donate their eTokens without checking whether the account stayed healthy afterward. The account went deliberately insolvent, and a second attacker address liquidated it at a discount that Euler’s liquidation logic paid out of the pool. Most of the funds were later returned.
Underneath the flash loan sits a business-logic flaw, an invariant the code was supposed to keep and didn’t. In Euler’s case, the invariant was something like “no account ends a transaction undercollateralized.” The flash loan only removed the capital requirement. Anyone with gas money can now test your edge cases at nine-figure scale, inside one block, and walk away clean if the test fails.
Prevention: write your invariants down in plain English before you write the code, then encode them. Foundry’s invariant testing, Echidna and Medusa, will run long random sequences of calls against properties like “total borrows never exceed total deposits” or “every account is solvent at the end of every transaction.” Every new function, especially one added in an upgrade, runs against the full suite. A missing health check is exactly what unit tests pass and invariant tests catch.
Upgradeable contracts fail at the seams nobody tests
On November 6, 2017, a user called initWallet on the shared library contract behind Parity’s multisig wallets. Nobody had ever initialized it. That made the caller its owner, and they then invoked kill, which self-destructed the library and froze the Ether in hundreds of wallets that delegated their logic to it. That ether is still stuck.
Wormhole is the cousin. On February 2, 2022, its Solana side lost $326 million when an attacker got a forged message past guardian signature verification by passing in a fake system account to a deprecated function that never checked which account it was reading. It’s a verification bug, not a proxy bug, but it rhymes: a component trusted input it should have checked.
With a proxy, code and state sit in different contracts, and the mistakes hide in the gap. Uninitialized implementations. Storage layout collisions after an upgrade. An initializer that can be called twice. A delegatecall into code that can destroy itself. Since the Dencun upgrade in March 2024, SELFDESTRUCT no longer wipes the code of an existing contract on Ethereum mainnet, so Parity’s exact ending can’t repeat there. The uninitialized-owner half absolutely can.
Prevention: in OpenZeppelin’s upgradeable pattern, use the initializer modifier on every initialize function and call disableInitializers in the implementation’s constructor, so nobody can claim the logic contract directly. Run a storage layout diff on every upgrade, with the OpenZeppelin Upgrades plugins or forge inspect. Prefer audited UUPS or transparent proxies over hand-rolled delegatecall. On Solana, check every account’s owner and address before trusting its data.
The biggest losses now happen before a transaction reaches the chain
On February 21, 2025, Bybit’s signers approved what their screens showed as a routine transfer from an Ethereum cold wallet secured by a Safe multisig. It wasn’t routine. Attackers had compromised a Safe{Wallet} developer machine and slipped malicious JavaScript into the interface Bybit used, so the signers actually approved a delegatecall that replaced the wallet’s implementation with attacker code. The FBI attributed the theft to North Korea’s TraderTraitor group. On October 29, 2026, Binance hot wallets were drained of about $6.3 billion by the exchange’s own estimate, in an incident the FBI has tied to the same North Korean group, built on validly signed authorizations. It’s still being traced.
At Bybit, the contracts were fine. They did exactly what a valid quorum of signatures told them to do. The weakness was the gap between what signers saw and what they signed. That’s an access control failure living in a browser tab. Chainalysis’s 2025 report says North Korean attacks made up a record 76% of all service compromises that year. My read is that well-funded attackers have concluded that keys and signers are cheaper targets than audited code. To be clear, that conclusion is mine. The report only counts the losses.
Prevention: verify every multisig transaction’s calldata independently of the interface that proposed it, on a separate device, and decode it before signing. Simulate it, and compare the Safe transaction hash against one computed by an independent tool. Block delegatecall from treasury multisigs with a safeguard unless a specific operation needs it. And rehearse the moment when the screen and the device disagree, because the device wins every time.
An audit is a snapshot, not a warranty
“We got audited, so we’re safe.”
It’s the line you hear at every launch party. Here’s the problem. An audit tells you what a specific team found in a specific commit over a specific number of weeks. It doesn’t cover the upgrade you ship in month four, the compiler bug nobody had found yet, or the laptop of whoever maintains your multisig interface. Euler had been through multiple audits before March 2023.
That isn’t an argument against audits. It’s an argument against treating the PDF as the security program. Stack it with invariant tests in CI, a public bug bounty, monitoring with an automatic pause, and a fresh review for every material upgrade.
| Vulnerability class | Famous exploit (year) | Reported loss | Prevention |
|---|---|---|---|
| Reentrancy | The DAO (2016) | Over 3.6 million ETH | Checks-effects-interactions, reentrancy guards |
| Compiler-level reentrancy | Curve and Vyper (2023) | $69.3 million | Pinned compiler, known-bugs review |
| Access control | Poly Network (2021) | $611 million | Narrow roles, multisig plus timelock |
| Oracle manipulation | Mango Markets (2022) | $115 million | TWAP plus independent feeds, collateral caps |
| Flash-loan logic flaw | Euler Finance (2023) | $197 million | Written invariants, Foundry and Echidna fuzzing on every upgrade |
| Uninitialized library | Parity multisig (2017) | Funds frozen, never recovered | Initializer guards, implementations locked at deployment |
| Signature verification | Wormhole (2022) | $326 million | Validate every account passed in |
| Front-end and signing compromise | Bybit (2025) | $1.5 billion | Independent calldata checks, delegatecall guards |
A pre-deployment checklist that catches the old bugs
Run this before mainnet, and again before every upgrade that touches funds.
- Every external call comes after state updates. Find each transfer, call and token hook, then read the lines above it.
- Every privileged function has a test from an unprivileged caller. If it doesn’t revert, you found the bug first.
- Upgrades and parameter changes sit behind a multisig and a timelock. No single key can move funds or swap logic.
- Prices come from at least two independent sources, with a TWAP. Borrowing pauses automatically when they diverge.
- Invariants are written in English and encoded in Foundry or Echidna. The suite runs in CI on every commit, upgrades included.
- Implementations are initialized or locked at deployment. Call disableInitializers in the constructor and diff storage layout on each upgrade.
- The compiler version is pinned and checked against its known bugs. Read the list for the exact version you deploy.
- Signers verify calldata on a second device. The interface is a suggestion, and the hardware wallet screen is the transaction.
- A bug bounty is live on launch day and sized to the TVL. The audit report is the floor, not the ceiling.
Old bugs only stay profitable because new money keeps parking right on top of them.
Press releases or guest posts published by Crypto Economy have been submitted by companies or their representatives. Crypto Economy is not part of any of these agencies, projects or platforms. At Crypto Economy we do not give investment advice, if you are going to invest in any of the promoted projects you should do your own research.
This story was originally published by Crypto Economy. Read the original article.





