Back to Blog

Pre-Audit Checklist: How to Prepare Your Smart Contracts for a Security Review

|Odin Scan Team
Pre-Audit Checklist: How to Prepare Your Smart Contracts for a Security Review

Most teams treat an audit like dropping their car off at the mechanic. Hand over the keys, wait for the report, fix what they find. That approach burns money.

Auditors bill by the hour or by the week. Every hour they spend deciphering your project structure, figuring out your deployment flow, or reading uncommented code is an hour they are not spending finding vulnerabilities. A well-prepared codebase gets a better audit in less time.

We have reviewed hundreds of repositories through Odin Scan. These are the patterns that separate codebases that are audit-ready from codebases that waste half the engagement on setup.


1. Freeze the Code

An audit reviews a specific commit hash. If you keep pushing code during the engagement, the auditors are reviewing a moving target. Findings become invalid. New bugs get introduced into files already reviewed.

Freeze the codebase before the audit starts. Create a branch or tag specifically for the audit. All development continues on a separate branch. If you absolutely must fix something mid-audit, communicate the change to the auditors and document which commit they should review.

# Tag the audit commit
git tag -a audit-v1 -m "Code freeze for security audit"
git push origin audit-v1

2. Write a Scope Document

Auditors need to know what to look at and what to ignore. A scope document saves days of back-and-forth.

Include:

  • Files in scope: exact file paths, not "the contracts directory"
  • Files out of scope: third-party libraries, mock contracts, test helpers
  • Lines of code: total nSLOC (normalized source lines of code) for the in-scope files
  • External dependencies: which protocols you integrate with (Uniswap, Chainlink, Aave, etc.)
  • Deployment targets: which chains and which compiler versions
  • Known issues: bugs you already know about and have accepted the risk on
  • Previous audit reports: links to any prior audits on the same codebase
# Count nSLOC for in-scope files using cloc or solidity-metrics
cloc --include-lang=Solidity src/

3. Document Your Architecture

A one-page architecture overview cuts hours off the audit ramp-up. It does not need to be a formal design document. A markdown file with:

  • System diagram: which contracts exist, how they interact, what calls what
  • Trust assumptions: who is the admin? What can the admin do? Is admin trust acceptable for your threat model?
  • Value flows: where does money enter the system, where does it exit, what paths does it take
  • Upgrade mechanism: proxy pattern, diamond, immutable, or something custom
  • Oracle dependencies: which price feeds, what staleness thresholds, how are they composed
## Architecture Overview

### Contracts
- Vault.sol: holds user deposits, issues shares
- Strategy.sol: deploys vault funds to Aave/Compound
- Oracle.sol: wraps Chainlink feeds with staleness checks

### Trust Model
- Admin can pause deposits but cannot withdraw user funds
- Strategy allocation changes require a 48-hour timelock

### External Dependencies
- Chainlink ETH/USD feed (0x...)
- Aave V3 lending pool on Ethereum mainnet

Auditors who understand the system find bugs faster. Auditors who are guessing at the architecture waste time on false leads.


4. Ensure Everything Compiles and Tests Pass

This sounds obvious. It is not obvious how often it fails.

Before handing off the codebase:

# For Hardhat/Foundry projects
forge build --force
forge test

# For Anchor projects
anchor build
anchor test

# For CosmWasm projects
cargo build --release --target wasm32-unknown-unknown
cargo test

If your tests require specific environment variables, RPC endpoints, or local services, document the setup. If an auditor cannot compile and run your tests within 15 minutes of cloning the repo, you have already lost time.


5. Run Static Analysis First

Do not pay auditors to find bugs that free tools catch automatically. Run the standard static analysis tools before the engagement and fix everything they flag.

For Solidity:

# Slither catches low-hanging fruit
slither . --filter-paths "test|script|lib"

# Aderyn for additional Solidity-specific checks
aderyn .

For Rust/Anchor/CosmWasm:

# Clippy with strict warnings
cargo clippy -- -D warnings

# cargo-audit for known dependency vulnerabilities
cargo audit

Every issue an auditor finds that Slither or Clippy would have caught is wasted audit budget. Fix the mechanical issues first so auditors can focus on the logic bugs that require human reasoning.


6. Write Meaningful Tests

Auditors look at your test suite to understand intended behavior. Tests that only cover the happy path tell them nothing about edge cases.

For every external function, include at least:

  • Happy path: normal usage works as expected
  • Access control: unauthorized callers are rejected
  • Edge cases: zero amounts, maximum values, empty arrays
  • Revert conditions: every require or revert has a test that triggers it
// Good: tests both success and failure
function test_withdraw_succeeds() public {
    deposit(user, 1 ether);
    vm.prank(user);
    vault.withdraw(1 ether);
    assertEq(address(user).balance, 1 ether);
}

function test_withdraw_reverts_insufficient_balance() public {
    vm.prank(user);
    vm.expectRevert("Insufficient balance");
    vault.withdraw(1 ether);
}

function test_withdraw_reverts_zero_amount() public {
    vm.prank(user);
    vm.expectRevert("Zero amount");
    vault.withdraw(0);
}

Auditors use failing test cases to verify their findings. If your test suite is thin, they spend time writing their own PoCs instead of finding more bugs.


7. Label Your Invariants

Every protocol has invariants: conditions that must always hold. Document them explicitly, either as comments or as dedicated invariant tests.

/// INVARIANT: totalShares * pricePerShare == totalAssets (within rounding)
/// INVARIANT: no user can withdraw more than they deposited + earned yield
/// INVARIANT: vault balance >= sum of all user claims

Auditors specifically try to break invariants. If they do not know what the invariants are, they have to infer them from the code, which means they might miss the ones you care about most.

For Foundry users, write invariant tests that the fuzzer can hammer:

function invariant_totalSharesMatchAssets() public {
    assertApproxEqAbs(
        vault.totalShares() * vault.pricePerShare() / 1e18,
        vault.totalAssets(),
        1e6 // rounding tolerance
    );
}

8. Clean Up Dead Code

Unused functions, commented-out blocks, and experimental features that never shipped all waste audit time. Every line of code in scope gets reviewed. If it is not needed, remove it before the audit.

# Find unused internal functions in Solidity
# (Slither reports this as "dead-code")
slither . --detect dead-code

Dead code also introduces confusion. An auditor might spend hours analyzing a code path that is never actually reachable, then discover it is dead. That is time stolen from reviewing live code.


9. Pin Your Dependencies

Lock every dependency to a specific version. Auditable code means reproducible builds.

# Foundry: use exact commit hashes in foundry.toml remappings
[profile.default]
remappings = [
    "@openzeppelin/=lib/openzeppelin-contracts-v5.0.2/",
]
# CosmWasm: pin exact versions in Cargo.toml
[dependencies]
cosmwasm-std = "=2.2.0"
cw-storage-plus = "=2.0.0"

If a dependency updates mid-audit, the auditors are now reviewing code they have not seen. Pin everything. Update after the audit, not during.


10. Prepare Your Deployment and Governance Scripts

This is the one most teams miss entirely.

The Moonwell hack was not in a core contract. It was in a governance proposal script. Deployment scripts, migration scripts, and governance proposals are code. They interact with your contracts. They configure critical parameters. They deserve the same scrutiny.

Include in scope:

  • Deployment scripts (Hardhat deploy/, Foundry script/, Anchor migrations/)
  • Governance proposal scripts
  • Oracle configuration files
  • Parameter initialization values

If the auditors only review src/ and the bug is in scripts/, you paid for an incomplete audit.


The Pre-Audit Checklist

Print this. Check every box before you start the engagement.

  • Code frozen at a specific commit/tag
  • Scope document with file paths, nSLOC, and external dependencies
  • Architecture overview with trust model and value flows
  • All contracts compile with zero warnings
  • All tests pass on a fresh clone
  • Slither/Clippy run with zero unaddressed findings
  • Tests cover happy paths, access control, edge cases, and reverts
  • Invariants documented and tested
  • Dead code removed
  • Dependencies pinned to exact versions
  • Deployment and governance scripts included in scope

Odin Scan catches the mechanical issues before your audit starts, so auditors can focus on the logic bugs that matter. Run it on every PR as part of your CI/CD pipeline, and your codebase will be cleaner before the audit even begins. Start your free trial.