Smart Contract Security Audit Checklist (Solidity)

Type: Software Reference Confidence: 0.88 Sources: 7 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

#VulnerabilitySeveritySWC IDDetectionPrevention
1ReentrancyCriticalSWC-107Slither reentrancy-eth, MythrilChecks-Effects-Interactions + ReentrancyGuard
2Integer Overflow/UnderflowHighSWC-101Mythril symbolic executionSolidity 0.8.x+ (built-in) or SafeMath for <0.8
3Access Control (Unprotected Functions)CriticalSWC-105Slither unprotected-upgradeOpenZeppelin Ownable/AccessControl + explicit modifiers
4Front-Running (TX Order Dependence)HighSWC-114Manual reviewCommit-reveal schemes, batch auctions, Flashbots Protect
5Oracle ManipulationCriticalN/AManual review, historical analysisTWAP oracles, Chainlink price feeds, multi-oracle design
6Unchecked Return ValuesHighSWC-104Slither unchecked-lowlevelAlways check return of .call(), .send(), .transfer()
7Delegatecall to Untrusted CalleeCriticalSWC-112Slither delegatecall-loopNever delegatecall to user-supplied addresses
8tx.origin AuthenticationHighSWC-115Slither tx-originUse msg.sender instead of tx.origin for auth
9Flash Loan AttacksCriticalN/AManual review, invariant testingSame-block price checks, delay mechanisms, access control
10Storage Collision (Proxy Patterns)CriticalN/ASlither uninitialized-storageEIP-1967 storage slots, EIP-7201 namespaced storage
11DoS with Failed CallMediumSWC-113Slither calls-loopPull-over-push payment pattern
12Timestamp DependenceLowSWC-116MythrilAvoid block.timestamp for critical logic; 15s tolerance

Decision Tree

START
|-- Is the contract upgradeable (proxy pattern)?
|   |-- YES --> Check for storage collision (EIP-1967/7201), uninitialized proxy,
|   |           selfdestruct in implementation, and delegatecall safety. See #7, #10.
|   +-- NO  |
|-- Does the contract handle ETH or token transfers?
|   |-- YES --> Check for reentrancy (#1), unchecked returns (#6), flash loan vectors (#9).
|   |           Apply Checks-Effects-Interactions + ReentrancyGuard.
|   +-- NO  |
|-- Does the contract use external price data?
|   |-- YES --> Check for oracle manipulation (#5) and front-running (#4).
|   |           Verify TWAP window, Chainlink heartbeat, multi-oracle fallback.
|   +-- NO  |
|-- Does the contract have admin/owner functions?
|   |-- YES --> Check for access control (#3), centralization risks, privilege escalation.
|   |           Verify role separation, timelocks, multi-sig requirements.
|   +-- NO  |
+-- DEFAULT --> Run full Slither + Mythril scan, review all 12 vulnerability classes above.

Step-by-Step Guide

1. Set up the audit environment

Clone the repository, install dependencies, and verify the project compiles cleanly. [src4]

# Clone and set up
git clone <target-repo>
cd <target-repo>

# Install dependencies (Hardhat or Foundry)
npm install          # for Hardhat projects
# or
forge install        # for Foundry projects

# Compile to verify no errors
npx hardhat compile  # Hardhat
# or
forge build          # Foundry

Verify: Compilation succeeds with zero errors and zero warnings.

2. Run static analysis with Slither

Slither detects 90+ vulnerability patterns in seconds. Run it first to catch low-hanging fruit. [src4]

# Install Slither
pip install slither-analyzer

# Run full analysis
slither . --json slither-report.json

# Run targeted high-severity detectors
slither . --detect reentrancy-eth,reentrancy-no-eth,arbitrary-send-eth,\
unprotected-upgrade,suicidal,controlled-delegatecall,tx-origin

Verify: Review all High and Medium findings in slither-report.json.

3. Run symbolic execution with Mythril

Mythril explores execution paths to find vulnerabilities Slither misses (e.g., complex integer issues, path-dependent bugs). [src7]

# Install Mythril
pip install mythril

# Analyze a single contract
myth analyze contracts/Vault.sol --solv 0.8.20

# Deep analysis (more execution depth, slower)
myth analyze contracts/Vault.sol --execution-timeout 300 --max-depth 50

Verify: Review report for any SWC-101, SWC-107, or SWC-104 issues.

4. Fuzz with Echidna or Foundry

Property-based fuzzing discovers edge cases that static analysis cannot. Define invariants and let the fuzzer try to break them. [src5]

# Foundry fuzzing (built-in)
forge test --fuzz-runs 10000

# Echidna (Trail of Bits property-based fuzzer)
echidna . --contract TestVault --config echidna.yaml

Verify: forge test -vvv -- all fuzz tests pass with 10,000+ runs.

5. Manual code review

Systematic line-by-line review focusing on business logic, which tools cannot fully assess. [src2]

Key checklist items: all external/public functions have access control, state changes happen before external calls, no unbounded loops, all unchecked blocks are intentionally safe, proxy storage layout matches, events emitted for state changes.

Verify: Document each finding with severity, location, and recommended fix.

6. Write the audit report

Consolidate all findings into a structured report with severity classifications. [src5]

Verify: Every Critical and High finding has a recommended fix and has been discussed with the development team.

Code Examples

Solidity: Vulnerable vs Secure Withdrawal (Reentrancy)

// VULNERABLE: State update AFTER external call (SWC-107)
contract VulnerableVault {
    mapping(address => uint256) public balances;
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        balances[msg.sender] = 0; // Too late -- attacker re-enters
    }
}

// SECURE: Checks-Effects-Interactions + ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) public balances;
    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        balances[msg.sender] = 0;                          // EFFECT first
        (bool success, ) = msg.sender.call{value: amount}(""); // INTERACTION last
        require(success, "Transfer failed");
    }
}

Solidity: Vulnerable vs Secure Access Control

// VULNERABLE: No access control (SWC-105) -- anyone can mint
contract VulnerableToken {
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

// SECURE: Role-based access with OpenZeppelin
import "@openzeppelin/contracts/access/AccessControl.sol";
contract SecureToken is AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); }
    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}

Solidity: tx.origin vs msg.sender

// VULNERABLE: tx.origin phishing (SWC-115)
require(tx.origin == owner, "Not owner");
// An attacker contract tricks owner into calling it,
// then calls this function -- tx.origin is still the owner

// SECURE: msg.sender cannot be spoofed
require(msg.sender == owner, "Not owner");

Anti-Patterns

Wrong: Unchecked low-level call return value

// BAD -- SWC-104: ignoring return value of .call()
function sendEth(address to, uint256 amount) external {
    to.call{value: amount}("");
    // If the call fails silently, ETH is lost
}

Correct: Always check call return value

// GOOD -- check return and revert on failure
function sendEth(address to, uint256 amount) external {
    (bool success, ) = to.call{value: amount}("");
    require(success, "ETH transfer failed");
}

Wrong: No reentrancy guard on fund-moving function

// BAD -- state update after external call
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
    balances[msg.sender] -= amount;  // too late!
}

Correct: Checks-Effects-Interactions pattern

// GOOD -- state update before external call + reentrancy guard
function withdraw(uint256 amount) external nonReentrant {
    require(balances[msg.sender] >= amount, "Insufficient");
    balances[msg.sender] -= amount;  // effect first
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "Transfer failed");
}

Wrong: Unbounded loop over dynamic array (DoS)

// BAD -- SWC-113: array grows unbounded, eventually hits gas limit
function distributeRewards() external {
    for (uint i = 0; i < recipients.length; i++) {
        payable(recipients[i]).transfer(rewards[i]);
    }
}

Correct: Pull-over-push payment pattern

// GOOD -- each user withdraws their own rewards
mapping(address => uint256) public pendingRewards;
function claimReward() external {
    uint256 reward = pendingRewards[msg.sender];
    require(reward > 0, "No reward");
    pendingRewards[msg.sender] = 0;
    (bool ok, ) = msg.sender.call{value: reward}("");
    require(ok, "Transfer failed");
}

Wrong: Using tx.origin for authentication

// BAD -- SWC-115: vulnerable to phishing via relay contract
require(tx.origin == owner, "Not authorized");

Correct: Use msg.sender for authentication

// GOOD -- msg.sender cannot be spoofed by intermediate contracts
require(msg.sender == owner, "Not authorized");

Common Pitfalls

Diagnostic Commands

# Run Slither static analysis (all detectors)
slither . --json report.json

# Run Slither with specific high-severity detectors only
slither . --detect reentrancy-eth,reentrancy-no-eth,arbitrary-send-eth,controlled-delegatecall,unprotected-upgrade

# List all Slither detectors and their severity
slither . --list-detectors

# Run Mythril on a specific contract
myth analyze contracts/MyContract.sol --solv 0.8.20

# Run Mythril on deployed contract (requires RPC)
myth analyze --address 0xCONTRACT --rpc https://eth-mainnet.g.alchemy.com/v2/KEY

# Run Foundry fuzz tests (10k runs)
forge test --fuzz-runs 10000 -vvv

# Run Echidna property-based fuzzer
echidna . --contract MyContractTest --config echidna.yaml --test-mode assertion

# Verify contract source matches deployed bytecode
forge verify-contract --chain-id 1 --compiler-version 0.8.20 0xADDRESS contracts/MyContract.sol:MyContract

# Generate storage layout diff for proxy upgrades
forge inspect MyContractV1 storage-layout > v1.json
forge inspect MyContractV2 storage-layout > v2.json
diff v1.json v2.json

Version History & Compatibility

Solidity VersionStatusKey Security ChangesMigration Notes
0.8.20+CurrentCustom errors (gas savings), transient storage (EIP-1153)Pin exact version; verify optimizer settings match audit
0.8.0-0.8.19SupportedBuilt-in overflow/underflow checks, ABI coder v2 defaultRemove SafeMath imports; review unchecked blocks
0.7.xLegacyLast version requiring SafeMathUpgrade to 0.8.x; remove SafeMath; test all arithmetic
0.6.xEOLNo overflow protection, different ABI encodingMust upgrade; extensive re-audit required
0.4.x-0.5.xEOLMany known vulnerabilities, no built-in protectionsFull rewrite recommended

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Deploying contracts that handle user funds (DeFi, vaults, bridges)Building a read-only contract with no value transferBasic unit testing for view-only contracts
Launching a token (ERC-20, ERC-721, ERC-1155)Writing a private/internal test contract on a devnetFoundry unit tests for development testing
Implementing upgradeable proxy patternsContract is a simple wrapper around a well-audited libraryCode review of integration points only
Building cross-chain bridges or oracle integrationsSmart contract is on a non-EVM chain (Solana, Aptos)Chain-specific audit tools (Anchor, Move Prover)
Post-hack incident response and forensic analysisYou need a gas optimization review onlyGas profiling tools (forge snapshot, hardhat-gas-reporter)

Important Caveats

Related Units