Smart Contract Security Audit Checklist (Solidity)
What is the smart contract security audit checklist (Solidity)?
TL;DR
- Bottom line: Reentrancy, access control flaws, and integer overflow/underflow are the top 3 smart contract vulnerabilities; a comprehensive audit combines manual review, static analysis (Slither/Mythril), fuzzing (Echidna), and formal verification.
- Key tool/command:
slither . --detect reentrancy-eth,reentrancy-no-eth,unprotected-upgrade,arbitrary-send-eth - Watch out for: Reentrancy in cross-contract calls -- the #1 cause of DeFi exploits, responsible for billions in losses (The DAO, Curve, Euler).
- Works with: Solidity 0.4.x-0.8.x on any EVM-compatible chain (Ethereum, Polygon, Arbitrum, BSC, Optimism).
Constraints
- Automated tools catch only ~30-50% of vulnerabilities -- always combine with manual expert review
- Solidity 0.8.x+ has built-in overflow protection via checked arithmetic; for 0.7.x and below, SafeMath is mandatory
- Never deploy unaudited contracts that handle user funds -- immutability means bugs cannot be patched post-deployment without proxy patterns
- Audit the deployed bytecode, not just source -- compiler optimizations and versions can introduce unexpected behavior
- Re-audit after every significant code change -- even single-line changes can introduce critical vulnerabilities
- Gas optimizations can introduce security issues -- never sacrifice safety for gas savings
Quick Reference
| # | Vulnerability | Severity | SWC ID | Detection | Prevention |
|---|---|---|---|---|---|
| 1 | Reentrancy | Critical | SWC-107 | Slither reentrancy-eth, Mythril | Checks-Effects-Interactions + ReentrancyGuard |
| 2 | Integer Overflow/Underflow | High | SWC-101 | Mythril symbolic execution | Solidity 0.8.x+ (built-in) or SafeMath for <0.8 |
| 3 | Access Control (Unprotected Functions) | Critical | SWC-105 | Slither unprotected-upgrade | OpenZeppelin Ownable/AccessControl + explicit modifiers |
| 4 | Front-Running (TX Order Dependence) | High | SWC-114 | Manual review | Commit-reveal schemes, batch auctions, Flashbots Protect |
| 5 | Oracle Manipulation | Critical | N/A | Manual review, historical analysis | TWAP oracles, Chainlink price feeds, multi-oracle design |
| 6 | Unchecked Return Values | High | SWC-104 | Slither unchecked-lowlevel | Always check return of .call(), .send(), .transfer() |
| 7 | Delegatecall to Untrusted Callee | Critical | SWC-112 | Slither delegatecall-loop | Never delegatecall to user-supplied addresses |
| 8 | tx.origin Authentication | High | SWC-115 | Slither tx-origin | Use msg.sender instead of tx.origin for auth |
| 9 | Flash Loan Attacks | Critical | N/A | Manual review, invariant testing | Same-block price checks, delay mechanisms, access control |
| 10 | Storage Collision (Proxy Patterns) | Critical | N/A | Slither uninitialized-storage | EIP-1967 storage slots, EIP-7201 namespaced storage |
| 11 | DoS with Failed Call | Medium | SWC-113 | Slither calls-loop | Pull-over-push payment pattern |
| 12 | Timestamp Dependence | Low | SWC-116 | Mythril | Avoid 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
- Assuming Solidity 0.8.x eliminates all overflow risks: Built-in checks only apply to normal arithmetic. Code inside
unchecked { }blocks bypasses overflow protection. Fix: grep foruncheckedand verify each usage is safe. [src6] - Trusting msg.value in loops: In a loop,
msg.valuedoes not decrease per iteration -- the same ETH can be "spent" multiple times. Fix: track remaining value in a local variable and decrement it. [src2] - ERC-20 approve race condition: Changing allowance from N to M allows a spender to front-run and spend N+M. Fix: use
increaseAllowance/decreaseAllowanceor approve to 0 first (required for USDT). [src5] - Assuming private state variables are secret: All on-chain storage is publicly readable regardless of visibility. Fix: never store secrets in contract storage. [src2]
- Forgetting to verify proxy storage layout: Adding or reordering state variables in an upgradeable contract causes storage collision. Fix: use OpenZeppelin's
@openzeppelin/upgradesplugin for automatic layout checks. [src3] - Ignoring compiler version pinning: Using
pragma solidity ^0.8.0means different auditors may compile with different versions. Fix: pin to exact version likepragma solidity 0.8.20;. [src6] - Not testing with forked mainnet state: Unit tests with mocks miss real-world interactions (fee-on-transfer tokens, rebasing tokens). Fix: use
forge test --fork-urlto test against production state. [src4]
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 Version | Status | Key Security Changes | Migration Notes |
|---|---|---|---|
| 0.8.20+ | Current | Custom errors (gas savings), transient storage (EIP-1153) | Pin exact version; verify optimizer settings match audit |
| 0.8.0-0.8.19 | Supported | Built-in overflow/underflow checks, ABI coder v2 default | Remove SafeMath imports; review unchecked blocks |
| 0.7.x | Legacy | Last version requiring SafeMath | Upgrade to 0.8.x; remove SafeMath; test all arithmetic |
| 0.6.x | EOL | No overflow protection, different ABI encoding | Must upgrade; extensive re-audit required |
| 0.4.x-0.5.x | EOL | Many known vulnerabilities, no built-in protections | Full rewrite recommended |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Deploying contracts that handle user funds (DeFi, vaults, bridges) | Building a read-only contract with no value transfer | Basic unit testing for view-only contracts |
| Launching a token (ERC-20, ERC-721, ERC-1155) | Writing a private/internal test contract on a devnet | Foundry unit tests for development testing |
| Implementing upgradeable proxy patterns | Contract is a simple wrapper around a well-audited library | Code review of integration points only |
| Building cross-chain bridges or oracle integrations | Smart contract is on a non-EVM chain (Solana, Aptos) | Chain-specific audit tools (Anchor, Move Prover) |
| Post-hack incident response and forensic analysis | You need a gas optimization review only | Gas profiling tools (forge snapshot, hardhat-gas-reporter) |
Important Caveats
- The SWC Registry has not been actively maintained since 2020. For current classifications, use the EEA EthTrust Security Levels spec (v2, Dec 2023) and the Smart Contract Security Verification Standard (SCSVS).
- No audit guarantees zero bugs. Audits reduce risk but cannot eliminate it. Bug bounty programs (Immunefi, HackerOne) provide ongoing coverage.
- DeFi composability means your contract's security depends on every contract it interacts with. A secure contract calling an insecure oracle or token can still be exploited.
- Formal verification (Certora, K Framework) provides mathematical proofs for specific properties but cannot cover all possible attack vectors.
- Layer 2 chains (Optimism, Arbitrum, zkSync) may have different EVM opcodes or gas costs that affect security assumptions.