Skip to main content
Web310 min readUpdated

Smart Contract Security: A Pre-Audit Checklist for Solidity Engineers

The concrete review passes that catch most Solidity vulnerabilities before an auditor sees them: access control, reentrancy beyond the obvious cases, oracle assumptions, upgrade safety, and invariant testing with Foundry.

Summary

Audits find bugs; they do not find missing design. The highest-value work happens before the audit: writing down every invariant, enumerating every privileged action and who can call it, testing every external-call boundary for reentrancy including read-only reentrancy, and fuzzing the accounting until it breaks. Teams that arrive at an audit with an invariant suite get findings about edge cases. Teams that arrive without one get findings about their architecture, and those cost months.

SolidityFoundrySlitherEchidnaOpenZeppelin

Pass 1: write the invariants down

Before reading a line of code, write the properties that must hold for all time, in plain English, in the repo. Then encode each as a Foundry invariant test. This exercise alone surfaces design contradictions that no line-by-line review will catch, because it forces you to state what the system is supposed to guarantee.

  • Conservation: the sum of all user claims never exceeds the assets held.
  • Monotonicity: indices, nonces, and accrual counters only move one direction.
  • Solvency: for every account, a stated risk condition holds after every state transition.
  • Authorization: no state variable in the privileged set changes without a call from the privileged role.
  • Liveness: for every position, some actor has a profitable action that resolves it.

Pass 2: enumerate privileged actions

Build a table with one row per external function that changes state: the function, the modifier guarding it, who holds that role, and what happens if that key is compromised. Most access-control bugs are not missing modifiers — they are modifiers that guard the wrong thing, or a role that turns out to be far more powerful than the team assumed.

  • Every initializer is guarded and cannot be called twice — the single most common upgradeable-contract failure.
  • Ownership transfer is two-step (propose then accept); a one-step transfer to a typo'd address is unrecoverable.
  • No role can unilaterally move user funds, or if one can, that is documented in the README in bold and behind a timelock.
  • Emergency pause covers the entry points that matter and, critically, does not block withdrawals or liquidations — a pause that traps user funds converts an incident into an insolvency.
  • Parameter setters have bounds. A setFee function that accepts 100% is a rug vector regardless of intent.

Pass 3: every external call is a yield point

Checks-effects-interactions is necessary and not sufficient. Any call to an address you do not control — including an ERC-20 transfer, since tokens with hooks such as ERC-777 and ERC-721 callbacks re-enter — hands execution to an adversary in the middle of your function.

  • State updated before every external call, without exception.
  • Reentrancy guards on entry points that touch shared accounting, including view-adjacent paths.
  • Read-only reentrancy considered: a getter that reads mid-update state can be called by an integrator during your callback and return a value that is briefly wrong. This is the class that broke several Curve-integrated protocols.
  • Return values of low-level calls checked; use SafeERC20 for tokens that return nothing (USDT) or false instead of reverting.
  • Fee-on-transfer and rebasing tokens handled explicitly — measure balance before and after, never trust the amount argument — or rejected at listing time in writing.

Pass 4: arithmetic and price assumptions

Solidity 0.8 gives you overflow reverts, which removes one bug class and adds another: a revert in an accrual path can freeze a protocol permanently. Check that no arithmetic on a path that must always succeed — liquidation, withdrawal, accrual — can revert on extreme but reachable inputs.

  • Rounding direction always favours the protocol, and this is asserted in tests, not assumed.
  • Decimals normalized at the boundary: 6-decimal USDC and 18-decimal DAI in the same expression is a recurring source of 12-orders-of-magnitude errors.
  • Every oracle read checks staleness, non-positive answers, and, on L2s, sequencer uptime feeds.
  • No spot AMM price used for valuation of anything.
  • Division before multiplication eliminated wherever precision loss compounds.

Pass 5: tooling, in this order

  1. forge test with branch coverage, targeting 100% on anything touching value.
  2. Slither for the cheap static wins; triage and suppress with justification comments rather than ignoring output wholesale.
  3. Foundry invariant runs with high depth against a handler that constrains the fuzzer to realistic action sequences — an unconstrained fuzzer mostly reverts and proves nothing.
  4. Echidna or Medusa for property fuzzing where the state space is deep.
  5. Fork tests replaying real historical blocks, including the volatile ones, against your deployed parameters.
  6. Only then, book the audit.

One habit worth more than any tool: after every finding — yours, a peer's, or an auditor's — add the failing case to the invariant suite before fixing it. The suite becomes an accumulating record of everything the system has ever gotten wrong, and it is the reason regressions stop recurring.

Frequently asked questions

How much does a smart contract audit cost?
As of 2026, reputable firms typically quote $15,000 to $80,000 for a focused protocol, scaling with lines of code and mechanism novelty. Competitive audit platforms can be cheaper for well-specified scopes. Arriving with full test coverage and written invariants materially reduces both cost and remediation time.
Is one audit enough?
For anything holding meaningful value, no. The realistic baseline is internal invariant testing, one or two independent audits, a public bug bounty scaled to total value locked, and monitoring with an incident runbook. Audits are a point-in-time snapshot of a codebase that keeps changing.
What is read-only reentrancy?
When a contract's view function returns inconsistent state during an external call it made mid-update. The attacker does not re-enter your write path at all — they call a third protocol that reads your getter while your state is momentarily wrong, and profit there. Nonreentrant modifiers on write functions do not prevent it.

Building something like this?

I'm Harsh Mittal — I build production systems across Web3, AI, and financial infrastructure: smart contracts and DeFi protocols, RAG pipelines and LLM agents, market data infrastructure, and the interfaces on top of them. If this is the kind of problem you're working on, I can help you ship it.