Over $3 billion was drained from DeFi protocols in 2022 alone. The attackers were not sophisticated state-sponsored hackers using zero-days. Most exploits came down to a small number of well-understood vulnerability classes that should have been caught before deployment — reentrancy, access control mistakes, oracle manipulation, and misconfigured upgrade patterns. These are not novel discoveries. They appear in audit reports year after year. What changes is which protocol gets hit next.

This guide covers the six most common vulnerability classes and the pre-deployment process that surfaces them before they become exploits. It is written from the perspective of a team that has shipped and internally reviewed production smart contracts since the early days of Ethereum — we have seen these failures in real code, not just in textbooks.

Why mainnet is different from testnet

A contract that works correctly on a testnet has proven one thing: it produces the expected output for the expected inputs. Mainnet adds three new adversaries that testnets don't meaningfully replicate.

Economic incentives. On testnet there is no money. On mainnet, a single critical bug in a DeFi protocol can yield millions in a single transaction. The incentive to find that bug — and the sophistication of the attackers who look for it — scales with the value locked.

Immutability. You can redeploy a testnet contract in seconds. On mainnet, a deployed contract is permanent. Even with upgradeable proxy patterns, the damage from an exploit before a pause can be irreversible.

Composability. On mainnet your contract will interact with every other contract on the chain — including ones you did not design for. Flash loans, which don't meaningfully exist on testnets, allow attackers to temporarily control enormous capital within a single transaction. That changes the threat model for anything that touches a price oracle or relies on a naive snapshot of on-chain state.

The six vulnerability classes to audit against

1. Reentrancy

The canonical smart contract vulnerability. A reentrancy attack occurs when an external call to another contract allows that contract to call back into the calling contract before the first execution has finished — exploiting the gap between when a state update is initiated and when it is completed.

The fix is the checks-effects-interactions pattern: always update your internal state before making any external calls. Never send ETH or call an external contract before setting the internal accounting to its post-transfer state. Additionally, reentrancy guards (a simple mutex modifier) add a second layer of protection for functions that must make external calls before settling internal state.

OpenZeppelin's ReentrancyGuard is the standard implementation. Using it is a non-negotiable baseline for any function that transfers value.

2. Access control mistakes

The most common vulnerability class by volume. Access control failures range from the catastrophic (an administrative function callable by anyone) to the subtle (a function correctly gated in normal use but callable through an upgrade path). Common failure patterns:

The standard: use OpenZeppelin's AccessControl or Ownable, enumerate all privileged functions explicitly in your specification, and verify each one has exactly the access control it requires — not approximately, not by inference.

3. Integer overflow and underflow

Solidity 0.8.0 introduced built-in overflow checking, making unchecked arithmetic revert rather than wrap silently. If you are writing or maintaining contracts in older Solidity versions, you should be using SafeMath. If you are on 0.8+, unchecked arithmetic blocks (used for gas optimization) must be manually verified to be safe — they opt out of the protection.

Pay particular attention to division rounding in financial calculations. Integer division truncates toward zero, which in a protocol that calculates fees or interest can systematically benefit one party at the expense of the other in ways that compound over time.

4. Oracle manipulation and price feed attacks

Any contract that makes decisions based on an on-chain price is vulnerable to oracle manipulation if that price can be moved faster than the contract can react. The classic version is the spot price attack: borrow an asset via flash loan, use the borrowed liquidity to move a pool's spot price, call a protocol that reads that spot price for a calculation, profit from the distorted calculation, repay the flash loan in the same transaction.

The defenses: use time-weighted average prices (TWAPs) from established oracles like Chainlink or Uniswap v3 rather than spot prices; require price inputs to pass a sanity check against an independent reference; use slippage guards on any action that depends on a price. Never use a single on-chain AMM pool as your sole price source for anything consequential.

5. Flash loan attack vectors

Flash loans themselves are not a vulnerability — they are a legitimate primitive. The vulnerability is logic that assumes a user's capital is limited to what they hold at the start of a transaction. Any governance system that allows instant token voting without time-lock, any protocol that lets you enter and exit a privileged position within one transaction, any mechanism that assumes a balance snapshot taken at the start of a function will still hold at the end — all are vulnerable to flash loan amplification.

The fix is architectural: governance votes require tokens to be locked for a period before they count; sensitive operations require time-delays; balance snapshots use historical checkpoints. ERC-20Votes (in OpenZeppelin) implements the standard checkpoint pattern for governance tokens.

6. Upgradeability storage collisions

Upgradeable proxy patterns (UUPS, Transparent Proxy) are powerful but introduce a new failure class. The proxy and implementation contracts share storage by the proxy delegating calls to the implementation's bytecode while using the proxy's storage slots. If the implementation's storage layout changes between upgrades — even by adding a new variable at the top of the contract — every subsequent storage slot shifts, corrupting all previously stored values.

The rules: never change the order of existing state variables in an upgradeable contract. New variables must always be appended at the end. Use OpenZeppelin's upgrade safety checks (the Foundry or Hardhat upgrade plugins) as an automated gate — they will catch storage layout breaks before deployment. Never initialize state variables directly in an upgradeable contract; use an initialize function with an initializer modifier.

The pre-deployment security checklist

A process, not just knowledge. Run through this before any mainnet deployment.

Post-deployment: monitoring is part of security

Deployment is not the end of the security lifecycle. Many exploits are detectable before they complete — they involve unusual transaction patterns, abnormal pool interactions, or repeated probing calls that look different from normal usage. Monitoring tools like Forta (a decentralized threat detection network), OpenZeppelin Defender, or custom event-based alerts can catch anomalies in real time and trigger a protocol pause before full drain occurs.

At minimum: set up alerts on large unexpected value movements, failed transactions from unusual addresses, and any calls to privileged functions. The first sign that something is wrong is often in the transaction mempool, before the block is confirmed. A functioning incident response process — who gets called, what the pause sequence is, how to communicate to users — should be defined before deployment, not during an active attack.

Security is a process, not a moment. It starts at the design stage and it doesn't end at deployment.

Our smart contract development process integrates all of these steps as standard — not as extras. Every contract we ship has gone through fuzz testing, static analysis, internal review, and gas optimization before it reaches our clients for sign-off. For high-value protocols we coordinate third-party audits and stay engaged through remediation. If you'd like to talk through the security architecture of something you're building, that's exactly the kind of conversation we're built for.