Kevin Riedl

7 min read · 26 May 2026

Smart Contract Security Checklist: 30 Items We Verify Before External Audit

We do not audit. We ship and harden smart contract code, then external firms audit it. Before that audit window opens, we run this 30-item pre-audit checklist across 6 categories: compiler and toolchain, access control and roles, arithmetic and overflow, external calls and reentrancy, upgradability and storage, gas and DoS surfaces. Every item below has burned us or a peer on a real engagement. Clearing all 30 has cut auditor finding counts on our handovers by roughly 60 to 80 percent in our engagement history, which means cheaper audits and faster mainnet.

This is the list we wish someone had handed us on day one of Scramble Pay, Quivr, Lightbridge, and our Account Abstraction work. We do not call it an audit. It is a hardening review, designed to make the audit boring.

Going to audit soon?

 Book Free Consultation

What does a smart contract pre-audit hardening review actually check?

Six categories, five items each, thirty items total. We treat the checklist as a gate. If an item fails, the contract does not leave our repo until it is fixed or explicitly waived in writing by the client with a reason. We pair this with our TDD workflow and QA service so every fix lands with a regression test.

Category 1: Compiler and toolchain (items 1 to 5)

  1. Pinned Solidity version. Lock a single compiler version in foundry.toml or hardhat.config. Floating pragmas like ^0.8.20 mean two developers can compile two different bytecodes from the same source.
  2. Optimizer settings declared and reproducible. Document the optimizer runs value. Etherscan verification fails on mismatch and an auditor will burn an hour before flagging it.
  3. SMTChecker and Slither pass with zero unsuppressed findings. Every suppression has an inline comment naming the auditor or engineer who waived it. No silent suppressions.
  4. Foundry fuzz and invariant tests run in CI with a high seed count. We require at least 10,000 fuzz runs and 256 invariant runs per critical function before audit handoff.
  5. Dependency lockfile committed and audited. OpenZeppelin and Solady versions pinned. No git dependencies pointing at main. We diff every dependency upgrade.

Category 2: Access control and roles (items 6 to 10)

  1. Every privileged function has an explicit role check. Not onlyOwner by default. We map each function to a role and document who holds it at deployment.
  2. No EOA holds admin keys at mainnet launch. Multisig or timelock. Period. We have seen single-key admin compromise on peer projects and it ends the project.
  3. Two-step ownership transfer. Ownable2Step or equivalent. A typo on a one-step transfer is unrecoverable.
  4. Role renounce and revoke paths tested. Can the DEFAULT_ADMIN_ROLE actually be renounced without bricking the protocol? Test it.
  5. Initializer protection on upgradeable contracts. _disableInitializers() in the constructor of every implementation contract, no exceptions.

Category 3: Arithmetic and overflow (items 11 to 15)

  1. Solidity 0.8+ used and unchecked blocks justified inline. Every unchecked block has a comment explaining why overflow is impossible at the call site.
  2. Division order verified. Multiply before dividing wherever precision matters. We grep for / in financial paths and review each one.
  3. Fee and percentage math uses basis points or a defined precision constant. No raw decimals. Document the constant and use it everywhere.
  4. Token decimal mismatches handled. USDC has 6 decimals, WETH has 18. We test every pair with a fork test against mainnet token addresses.
  5. Casting checks. Every uint256 to uint128 or smaller has an explicit bound check or SafeCast.

Category 4: External calls and reentrancy (items 16 to 20)

  1. Checks-Effects-Interactions enforced. State updates happen before any external call. We review each external call by hand.
  2. ReentrancyGuard on every function that makes an external call and modifies state. Default on, opt out only with justification.
  3. Cross-function reentrancy tested. A reentrant call into a sibling function with shared state is the classic missed vector. We fuzz it.
  4. Untrusted token callbacks handled. ERC777, ERC1155 onERC1155Received, ERC721 onERC721Received. Assume any external token is hostile.
  5. Return data length checked on low-level calls. (bool ok, ) = target.call(...) with no return data check is a silent failure waiting to happen.
Kevin Riedl

"Hardening is what makes the audit boring. Boring audits ship."

Category 5: Upgradability and storage (items 21 to 25)

  1. Storage layout documented and diffed on every upgrade. We run forge inspect storage layout before and after, and reject any reordered slot.
  2. Storage gaps reserved on every upgradeable parent. uint256[50] private __gap; per OpenZeppelin convention.
  3. Constructor logic moved to initializer for proxies. Anything in a constructor on an upgradeable contract is a bug.
  4. UUPS authorizeUpgrade has a role check. Default OpenZeppelin scaffolding leaves it abstract. Make sure it is implemented and protected.
  5. Upgrade path tested end-to-end on a fork. Deploy V1, populate state, upgrade to V2, verify state intact. Mandatory.

Category 6: Gas and DoS surfaces (items 26 to 30)

  1. Unbounded loops removed or paginated. Any loop over an array that users can grow is a DoS vector. We refactor to pull-based patterns.
  2. Block gas limit headroom verified. No single function should exceed roughly 30 percent of the block gas limit under realistic state size.
  3. External call gas stipends handled. Forwarding all gas to untrusted contracts can enable griefing. We cap or use try/catch.
  4. Storage writes minimized in hot paths. Hot storage slots get packed. We measure with forge snapshot and reject regressions over 5 percent.
  5. Front-running and MEV surface documented. Commit-reveal, slippage params, or deadline params where relevant. If the protocol is exposed to sandwich attacks, the user knows before mainnet.

Why does this checklist exist instead of just hiring an auditor?

Auditors charge by the line of code and by the day. Sending unhardened code to an audit is paying senior engineers €2,000 to €4,000 per day to find bugs your own team could have caught with Slither and a fuzz harness. In our engagement history, projects that completed all 30 items before audit handoff came back with auditor finding counts in the single digits, most of them informational. Projects that skipped the checklist routinely came back with 30+ findings including criticals, which then required a re-audit at full cost.

This is the difference between Wavect and a generalist shop. See Wavect vs a generalist dev agency for the full context. We build Web3 systems with a security-first cadence baked into our blockchain service, so the audit becomes a sign-off rather than a rescue.

Who owns the checklist on a project?

The Wavect tech lead on the engagement. They sign off each item with a commit reference and a test reference. The client receives the signed checklist as part of the audit handover package, which the external auditor receives alongside the code. Auditors love this because it tells them where to focus their attention.

Final thoughts

30 items, 6 categories, one signed-off document per release. That is the pre-audit hardening review. We do not replace external auditors and we never claim to. What we do is hand them code that has already cleared the obvious vectors, so their fee buys you deep findings instead of obvious ones.

If you are about to ship a Solidity codebase to mainnet and you have not run a structured hardening pass, you are paying audit rates for work you could have done at engineering rates. Talk to us before you book the audit slot, not after.

Shipping smart contracts?

 Book Free Consultation
Kevin Riedl

7 min read · 26 May 2026