Smart contract security patterns we learned the hard way

After reviewing dozens of contracts and building our own, these are the patterns that separate secure code from code that's waiting to be exploited. Most of these cost someone real money.

Smart contract attack surface
Common vulnerability classes in Solidity contracts and their mitigations.

Why we wrote this

Since 2015 we've been reading, writing, and breaking smart contracts. By 2019 the ecosystem has matured enough that certain vulnerability classes keep appearing — not because developers don't care, but because the patterns aren't well documented in one place with real context.

This post is our internal checklist made public. We use these patterns every time we review a contract, and we're open-sourcing the thinking behind them.

1. Reentrancy — still the #1 killer

The DAO hack in 2016 made reentrancy famous, but it keeps showing up in subtler forms. The pattern is simple: an external call is made before state is updated, allowing the called contract to re-enter and exploit stale state.

The fix:

  • Follow checks-effects-interactions religiously. Check conditions, update state, then make external calls.
  • Use reentrancy guards (mutex locks) on functions that transfer value.
  • Be especially careful with callbacks in ERC-777 tokens and flash loan receivers.

What most guides miss: reentrancy isn't just about call.value(). Any external call — including token transfers with hooks — can re-enter your contract. Cross-function reentrancy (entering a different function that shares state) is harder to spot and equally dangerous.

2. Access control — who can call what?

We've seen contracts with perfectly secure logic that are completely owned because anyone can call the admin functions. This sounds obvious but it's surprisingly common, especially in:

  • Proxy upgrade patterns where the initializer isn't protected.
  • Multi-step deployment where permissions aren't set atomically.
  • Functions that were "temporary" during testing and never removed.

Our checklist:

  • Every external/public function needs an explicit access control decision.
  • Use OpenZeppelin's AccessControl or Ownable — don't roll your own.
  • Test the negative case: verify that unauthorized callers are rejected.
  • Review initializers in upgradeable contracts — they're constructors that anyone can call if unprotected.

3. Integer arithmetic — overflow, underflow, precision

Before Solidity 0.8 (which added built-in overflow checks), integer overflow and underflow were responsible for millions in losses. Even with 0.8+, precision loss in division operations and rounding errors in financial calculations remain dangerous.

Patterns we enforce:

  • Use SafeMath for pre-0.8 code. No exceptions.
  • Always multiply before dividing to minimize precision loss.
  • Use fixed-point arithmetic libraries for financial calculations.
  • Test edge cases: zero amounts, maximum uint256 values, dust amounts.

4. Oracle manipulation — garbage in, garbage out

Any contract that relies on external price data is only as secure as its oracle. We've seen protocols use spot DEX prices as oracles — which can be manipulated in a single transaction using flash loans.

What we recommend:

  • Use time-weighted average prices (TWAPs) instead of spot prices.
  • Multiple oracle sources with median aggregation.
  • Circuit breakers for extreme price movements.
  • Assume any on-chain price can be manipulated within a single block.

5. Front-running and MEV exposure

If your contract has transactions where the outcome depends on ordering, you have an MEV problem. This includes DEX trades, liquidations, NFT mints, and any auction mechanism.

This is an area we're actively researching. The design space for MEV-resistant mechanisms is still young, but some patterns are already clear:

  • Commit-reveal schemes for auctions and votes.
  • Slippage protection and deadline parameters for swaps.
  • Private mempools / encrypted transaction pools (still experimental).
  • Design mechanisms where ordering doesn't matter (batch auctions).

6. Upgrade risks — the double-edged sword

Upgradeable contracts (proxy patterns) give you flexibility but introduce a massive trust surface. An upgrade can change any logic, making the entire security model dependent on the upgrade key.

  • Use timelocks on upgrades — give users time to exit.
  • Multi-sig for upgrade authority, not a single EOA.
  • Storage layout compatibility between versions — storage collisions are silent and deadly.
  • Consider whether upgradeability is actually needed. Immutable contracts are maximally secure.

What we're open-sourcing

We're packaging these patterns into a reusable checklist and a set of Solidity test templates that cover the common vulnerability classes. The goal is to make security review more systematic and less dependent on individual expertise.

This is the beginning of what will become our open-source security tooling — starting with patterns and checklists, eventually expanding into static analysis and automated testing.

Need a security review?

We review smart contracts and protocol designs for teams building on Ethereum. Our reviews are informed by the patterns above and years of hands-on experience.