Smart contracts don’t “ship,” they ossify. Once deployed, your mistakes become public infrastructure—often with money attached. An audit isn’t a rubber stamp; it’s a structured process to eliminate entire classes of failure before attackers do it for you.

Below is a pragmatic audit checklist we use to pressure-test contracts prior to mainnet, including what to verify, why it matters, and common failure modes.

1) Scope, threat model, and “what are we securing?”

Before reviewing a single line of Solidity, lock the scope and threat model.

  • Assets at risk: What can be stolen or frozen (tokens, NFTs, vault deposits, protocol fees)?
  • Trust assumptions: Admin keys, multisigs, oracles, off-chain signers, relayers, sequencers.
  • Attack surface: Upgradeability, bridges, callback hooks (ERC777), external integrations, cross-chain messages.
  • Invariants: Write them down in plain English (e.g., “Total shares must always map to total assets,” “No user can withdraw more than their balance,” “Liquidations cannot create bad debt beyond X”).

Opinionated take: if your team can’t clearly articulate invariants and trust assumptions, you’re not ready for an audit—you’re ready for architecture work.

2) Contract architecture and upgradeability

Architecture choices create permanent risk.

  • Upgrade pattern: Transparent proxy, UUPS, Diamond, immutable. Verify admin roles, upgrade permissions, and rollback/upgrade tests.
  • Initialization: Confirm initialize() can only run once and cannot be front-run. Ensure all inherited initializers execute.
  • Storage layout: For upgradeable contracts, verify storage gaps, ordering, and no variable type changes.
  • Dependency pinning: Lock compiler version and third-party dependencies. Audit vendored code or pinned commits.

Common failure: a missing initializer guard lets an attacker initialize your proxy and seize ownership.

3) Access control and privilege boundaries

Most real exploits are “auth bugs,” not math bugs.

  • Role design: Use Ownable/AccessControl correctly. Confirm least privilege.
  • Privileged functions inventory: List every admin-only function and verify intended limitations.
  • Two-step ownership transfers: Prefer transferOwnership + acceptOwnership patterns.
  • Multisig + timelock: For protocols handling value, a multisig isn’t optional; timelocks reduce governance key risk.
  • Emergency controls: Pause/unpause mechanics, but ensure pause can’t be abused to permanently lock funds.

Practical check: search for onlyOwner, onlyRole, auth, governance, and review those functions first—this is where “instant rug” bugs hide.

4) External calls, reentrancy, and callback hazards

Any external call is an adversarial boundary.

  • Reentrancy protections: Use Checks-Effects-Interactions, ReentrancyGuard, or pull-payment patterns.
  • ERC20 quirks: Handle non-standard tokens (no return value, fee-on-transfer, rebasing). Use SafeERC20.
  • Callbacks: ERC721/1155 receivers, ERC777 hooks, Uniswap V3 callbacks, flash loan callbacks—validate caller and state.
  • Call ordering: Ensure state updates happen before external calls where appropriate.

Example: a vault that calls token.transfer() before decrementing user shares is a classic reentrancy footgun.

5) Arithmetic, accounting, and precision

Solidity 0.8+ prevents overflow, but accounting can still be wrong.

  • Share accounting: Verify deposit/withdraw math, rounding direction, and dust handling.
  • Invariant checks: totalAssets >= totalDebt, sum(balances) == totalSupply (where applicable).
  • Decimals normalization: Consistently handle tokens with 6/8/18 decimals.
  • Fee logic: Protocol fees, performance fees, and withdrawal fees should be monotonic and bounded.

Real-world pattern: rounding in the attacker’s favor can become a “precision drain,” especially in share-based vaults.

6) DeFi-specific risks: oracles, MEV, and liquidity

If your contract touches pricing, assume adversarial markets.

  • Oracle validation: Source, heartbeat, decimals, stale price handling, and circuit breakers.
  • TWAP usage: Ensure observation windows are long enough; protect against short-term manipulation.
  • MEV considerations: Sandwich attacks on swaps, liquidation backruns, mint/redeem timing games.
  • Slippage controls: All swaps should take min-out parameters; avoid relying on on-chain spot prices.
  • Liquidity assumptions: Handling of low-liquidity tokens, rebase tokens, and tokens with transfer fees.

Concrete example: protocols that use a DEX spot price for collateral valuation often get drained via price manipulation within a single block.

7) Input validation and state machine correctness

Most vulnerabilities are “we forgot to forbid a bad state.”

  • Require statements: Validate addresses (non-zero), amounts (>0), and array length checks.
  • State transitions: Ensure functions cannot be called in invalid phases (e.g., settle before maturity).
  • Replay protection: For signatures and meta-transactions, use nonces and domain separators (EIP-712).
  • Time-based logic: Don’t rely on exact timestamps; consider block time variance.

Quick audit tactic: diagram the state machine and list “illegal transitions,” then search for code paths that permit them.

8) Token standards compliance and edge cases

Interoperability failures become security failures.

  • ERC20 behavior: Return values, approve front-running issues, permit (EIP-2612) correctness.
  • ERC721/1155: Receiver hooks, safe transfer behavior, and reentrancy during onERC721Received.
  • Permit signatures: Verify chain ID handling, nonce increments, and deadline checks.

If you integrate third-party tokens, treat them as hostile until proven otherwise.

9) Events, observability, and operational safety

Auditors look for exploitable bugs; operators need visibility.

  • Event completeness: Emit events for critical actions (upgrades, role changes, parameter updates, pausing).
  • Error messages: Custom errors improve clarity and reduce gas.
  • Parameter bounds: Enforce sensible min/max for fees, LTV, and rate parameters.

Opinionated take: “No events for admin changes” isn’t just bad UX—it’s a governance and incident-response risk.

10) Testing strategy: unit, integration, fuzzing, invariants

Tests are part of the audit surface.

  • Unit tests: Cover happy paths and expected reverts.
  • Integration tests: Fork mainnet and simulate real tokens and routers.
  • Fuzz/property tests: Use Foundry/Echidna to validate invariants under random actions.
  • Invariant testing: Continuously assert key properties (no negative balances, conservation rules).
  • Coverage: Aim for meaningful coverage, not vanity metrics.

A solid invariant test suite often catches what manual review misses—especially in vaults, AMMs, and lending logic.

11) Static analysis and tooling checks

Automate what can be automated.

  • Linters: Solhint, prettier, consistent style.
  • Static analyzers: Slither for common patterns; Mythril where applicable.
  • Gas snapshots: Prevent accidental gas regressions that break UX or DoS conditions.
  • Dependency scanning: Check known issues in libraries and pinned versions.

Treat tool findings as leads, not truth. False positives happen; false negatives happen more.

12) Deployment, config, and post-launch hardening

Many “audited” exploits happen after deployment due to config mistakes.

  • Deployment scripts reviewed: Verify constructor/initializer args and ownership assignment.
  • Admin key custody: Multisig set up, signers verified, recovery plan documented.
  • Timelock delays: Match your risk profile; publish governance processes.
  • Monitoring: Alerts on large transfers, role changes, pause events, oracle deviations.
  • Bug bounty: Launch a bounty (even small) and publish a disclosure channel.

Conclusion: audits are a process, not an event

A smart contract audit checklist isn’t about ticking boxes—it’s about systematically reducing attack surface and validating invariants under adversarial conditions. The best outcomes come when teams treat auditing as continuous: threat model early, test aggressively, constrain privileges, and operationalize safety with monitoring and controlled upgrades.

If you do only three things before mainnet: (1) lock down access control with a multisig + timelock, (2) run invariant fuzz tests on core accounting, and (3) review every external call like it’s an attacker. That alone prevents a surprising percentage of real-world losses.