DEV Community

Naren karthi
Naren karthi

Posted on

How to Audit a Smart Contract Like a Pro (Complete Guide + Tools)

How to Audit a Smart Contract Like a Pro

Complete Guide + Tool‑belt

Target audience: Solidity developers, security engineers, auditors‑in‑training, and anyone who wants to move from “code‑review” to a full‑blown professional audit.

What you’ll get:

  • A mind‑set that forces you to see every line of code as a possible attack surface.
  • A static‑analysis workflow (Slither, Mythril, Solhint) that you can drop into any CI pipeline.
  • A fuzz‑testing strategy (Foundry, Echidna) with concrete command‑lines and tuning tips.
  • A method for defining and automating invariants that catch subtle arithmetic or state‑consistency bugs.
  • A manual‑review checklist covering the classic and the emerging categories of vulnerabilities.
  • A quick‑start on bounty platforms (Code4rena, Sherlock, Immunefi) and how to turn audit work into reputation and cash.

Table of Contents

  1. The Auditing Mindset – “Assume Malicious”
  2. [Static Analysis – The First Line of Defense]
    1. Slither (100+ detectors)
    2. Mythril (symbolic execution)
    3. Solhint (style & best‑practice linter)
    4. CI Integration (GitHub Actions, GitLab CI, Foundry CI)
  3. [Dynamic & Fuzz Testing]
    1. Foundry’s built‑in fuzzer
    2. Echidna invariant testing
    3. Running a “1 M‑run” campaign – when is it enough?
  4. [Invariants – Turning Business Rules into Automated Checks]
    1. Writing invariants in Solidity (Echidna)
    2. Writing invariants in Foundry (cheatcodes)
    3. Example invariants for ERC‑20, ERC‑721, and custom logic
  5. [Manual Review Checklist – The Auditor’s “Cheat Sheet”]
    • Re‑entrancy & Call‑stack depth
    • Access control & role management
    • Oracle & price‑feed manipulation
    • Arithmetic precision & rounding errors
    • Upgradeability & proxy pitfalls
    • Gas‑related DoS, “unbounded loops”, and “out‑of‑gas” vectors
    • Tokenomics & economic attacks (mint caps, slippage, flash‑loan abuse)
    • Cross‑chain & bridge considerations
  6. [Bounty & Bug‑Finding Platforms]
    • Code4rena – competition format
    • Sherlock – “continuous bounty” model
    • Immunefi – high‑value “critical” contracts
    • How to build a reputation, write a solid report, and negotiate payouts
  7. [Putting It All Together – End‑to‑End Audit Playbook]
  8. [Further Reading & Community Resources]

1. Mindset: Assume Malicious – Every External Call Is a Trap, Every Variable Is Tainted

Auditing is not a code‑style review. It’s a mental exercise where you deliberately look for ways an attacker could subvert the intended behavior. The most powerful mental model is:

“Everything that can be influenced by an external actor is already under the attacker’s control.”

1.1. Taint‑Propagation Basics

Source of Taint Example Why it matters
msg.sender address owner = msg.sender; The caller can be any EOA or contract, possibly malicious.
msg.value require(msg.value >= price); An attacker can send 0 ether, overflow, or a malicious amount.
tx.origin require(tx.origin == admin); Bad – can be spoofed through a contract chain.
External read (call, staticcall, delegatecall) price = IOracle(oracle).getPrice(); The oracle contract may be compromised or return manipulated data.
Public storage variables (including public getters) public mapping(address => uint) balances; Anyone can read, but also indirectly influence via functions that modify them.
Return data from low‑level calls (bool ok, bytes memory ret) = target.call(...); If the called contract reverts or returns malformed data, your logic may behave unexpectedly.
Constructor arguments & immutable variables constructor(address _oracle) { oracle = _oracle; } If the constructor is called by a malicious deployer, the stored address can be a trap.
msg.data bytes calldata payload = msg.data; Manipulated calldata can cause parsing bugs.

Rule of thumb: If a variable ever receives data that originated outside the contract’s own code, treat it as *tainted and trace its flow to every place it is used.*

1.2. “Every External Call Is a Trap”

  1. Check the return value – do you require success?
  2. Guard against re‑entrancy – use the checks‑effects‑interactions pattern or OpenZeppelin’s ReentrancyGuard.
  3. Limit gas – if you need to interact with an untrusted contract, consider call{gas: 5000} to prevent DoS via out‑of‑gas.
  4. Validate the callee – is the address whitelisted? Is it a contract or an EOA? Use extcodesize or Address.isContract.

Pro tip: Write a tiny helper library SafeExternalCall that centralizes these checks. When you see target.call(...) in the codebase, you instantly know you need to check the helper for proper use.

1.3. “Every Variable Is Tainted”

  • Even internal variables like uint256 totalSupply; can become tainted if they are later derived from a user‑controlled source (e.g., totalSupply = totalSupply + mintedAmount; where mintedAmount is supplied by a mint(address to, uint256 amount) that lacks proper access control).

  • The moment you see a public or external function that updates a state variable, you must ask: Who is allowed to call it? If the access control is insufficient, the variable becomes an attack vector.

1.4. Quick‑Check Mental Checklist

Situation Question to Ask
address external = msg.sender; Can msg.sender be a contract that later calls back?
uint256 x = abi.decode(data, (uint256)); What if data is malformed? Will it revert or produce a bogus x?
target.call(abi.encodeWithSignature(...)) Do we check success? Do we limit gas? Do we verify the target address?
balances[msg.sender] += amount; Is amount bounded? Is there a mint cap?
owner = msg.sender; in the constructor Who deployed the contract?
price = IOracle(oracle).price(); Can the oracle be front‑run or compromised?

Keep this table on a sticky note while you code‑review – it forces you to ask the malicious version of every operation.


2. Static Analysis – The First Line of Defense

Static analysis runs without executing the contract, scanning the abstract syntax tree (AST) or bytecode for known patterns. Think of it as the “lint + security scanner” that catches low‑ hanging fruit before you even launch a fuzzer.

2.1. Slither – 100+ Built‑In Detectors

Slither is the de‑facto standard for Solidity static analysis. It parses the Solidity source and produces an easy‑to‑read report.

2.1.1. Installation

# Using pip (recommended)
python3 -m pip install slither-analyzer

# Or via Docker (useful for CI isolation)
docker pull trailofbits/eth-security-toolbox
Enter fullscreen mode Exit fullscreen mode

2.1.2. Running Slither

# Basic scan
slither src/**/*.sol

# Output as JSON (good for CI parsers)
slither src/**/*.sol --json ./reports/slither-report.json

# Limit to a specific detector (e.g., reentrancy)
slither src/**/*.sol --detect reentrancy
Enter fullscreen mode Exit fullscreen mode

2.1.3. Example Output (excerpt)

[Reentrancy] Potential reentrancy vulnerability in ContractA._withdraw()
 └─ src/ContractA.sol:78:23
   └─ call to external contract `msg.sender.call{value: amount}("")` without reentrancy guard.
Enter fullscreen mode Exit fullscreen mode

2.1.4. Extending Slither

You can write custom detectors in Python. A minimal custom detector that flags any delegatecall without a whitelist:

# detectors/delegatecall_without_whitelist.py
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification

class DelegatecallWithoutWhitelist(AbstractDetector):
    ARGUMENT = "delegatecall-whitelist"
    HELP = "Detect delegatecall to non‑whitelisted address"
    IMPACT = DetectorClassification.LOW
    CONFIDENCE = DetectorClassification.MEDIUM

    def _detect(self):
        results = []
        for contract in self.compilation_unit.contracts:
            for f in contract.functions:
                for node in f.nodes:
                    if node.type == "DELEGATECALL":
                        # simplistic check: look for a preceding require(isWhitelisted(addr))
                        if not any("isWhitelisted" in n.source_code for n in node.incoming_nodes):
                            results.append(self.generate_result([node]))
        return results
Enter fullscreen mode Exit fullscreen mode

Add to Slither:

slither src/**/*.sol --detectors detectors/delegatecall_without_whitelist.py
Enter fullscreen mode Exit fullscreen mode

2.2. Mythril – Symbolic Execution

Mythril goes a step further: it symbolically executes the contract bytecode, trying to find paths that violate certain constraints (e.g., integer overflow, unchecked send). It is slower than Slither but can uncover deeper logical bugs.

2.2.1. Installation

# Install via pip
pip install mythril

# Or Docker
docker pull mythril/myth
Enter fullscreen mode Exit fullscreen mode

2.2.2. Running Mythril

# Scan a Solidity file (Mythril compiles under the hood)
myth analyze src/Token.sol --solc-version 0.8.20 --max-depth 128

# Scan compiled bytecode (more deterministic)
myth analyze --bin-runtime <path-to-runtime-bytecode> --max-gas 8000000
Enter fullscreen mode Exit fullscreen mode

2.2.3. Interpreting Results

Mythril reports "issues" with a confidence level:

[Warning] Integer Overflow (PC 0x2f) 0x5a5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5
Enter fullscreen mode Exit fullscreen mode

Mythril also supports custom detectors written in Python. For example, a detector that flags any selfdestruct call after a certain block number (anti‑rug‑pull check).

2.3. Solhint – Linter & Style Guide

A tidy codebase makes static analysis easier and reduces human error. Solhint enforces community style guidelines and flags patterns that are prone to bugs (e.g., missing pragma solidity ^0.8.0;).

2.3.1. Installation

npm i -g solhint
Enter fullscreen mode Exit fullscreen mode

2.3.2. Running Solhint

solhint "src/**/*.sol"

# Use a custom config (solhint.json) to enable/disable specific rules
solhint -c ./solhint.json "src/**/*.sol"
Enter fullscreen mode Exit fullscreen mode

2.3.3. Sample solhint.json

{
  "extends": "solhint:recommended",
  "rules": {
    "compiler-version": ["error", "^0.8.0"],
    "func-visibility": ["error", {"ignoreConstructors": true}],
    "no-empty-blocks": "warn",
    "avoid-low-level-calls": "error",
    "max-line-length": ["warn", 120]
  }
}
Enter fullscreen mode Exit fullscreen mode

2.4. CI Integration – Run Every Commit

Static analysis should be automated. Below are ready‑to‑copy snippets for GitHub Actions, GitLab CI, and a generic Makefile.

2.4.1. GitHub Actions (.github/workflows/security.yml)

name: Security Checks

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  static-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python (for Slither & Mythril)
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Slither & Mythril
        run: |
          pip install slither-analyzer mythril

      - name: Install Solhint
        run: npm i -g solhint

      # ---------- Slither ----------
      - name: Run Slither
        run: |
          slither src/**/*.sol --json reports/slither.json
        continue-on-error: true # allow the job to continue for other checks

      # ---------- Mythril ----------
      - name: Run Mythril (limited depth for CI speed)
        run: |
          myth analyze src/**/*.sol --max-depth 64 || true

      # ---------- Solhint ----------
      - name: Run Solhint
        run: |
          solhint "src/**/*.sol" || true

      # ---------- Upload Artifacts ----------
      - name: Upload reports
        uses: actions/upload-artifact@v4
        with:
          name: security-reports
          path: reports/
Enter fullscreen mode Exit fullscreen mode

Why continue-on-error? You want the CI to surface warnings and let the pipeline proceed (e.g., to run tests). The final gate can be a separate “review required” step.

2.4.2. GitLab CI (.gitlab-ci.yml)

stages:
  - static

static-analysis:
  stage: static
  image: python:3.11
  before_script:
    - pip install slither-analyzer mythril solc-select
    - npm i -g solhint
    - solc-select install 0.8.20
    - solc-select use 0.8.20
  script:
    - slither src/**/*.sol --json slither-report.json || true
    - myth analyze src/**/*.sol --max-depth 64 || true
    - solhint "src/**/*.sol" || true
  artifacts:
    paths:
      - slither-report.json
    expire_in: 1 week
Enter fullscreen mode Exit fullscreen mode

2.4.3. Makefile for Local Development

.PHONY: lint slither myth all

# -------------------------------------------------
# Lint (Solhint)
lint:
    npm i -g solhint
    solhint "src/**/*.sol"

# -------------------------------------------------
# Slither
slither:
    slither src/**/*.sol --json ./reports/slither.json

# -------------------------------------------------
# Mythril (symbolic)
myth:
    myth analyze src/**/*.sol --max-depth 64

# -------------------------------------------------
# Run everything
all: lint slither myth
Enter fullscreen mode Exit fullscreen mode

Run locally with make all. The same targets can be referenced from CI containers.


3. Fuzzing – From Random Inputs to Real‑World Confidence

Static analysis finds known patterns. Fuzzing explores unknown paths by feeding massive numbers of random (or guided) inputs to the EVM.

3.1. Foundry’s Built‑In Fuzzer

Foundry (forge) is a fast Solidity testing framework with an integrated fuzzer.

3.1.1. Install Foundry


bash
curl

#SmartContracts #Security #Audit #Solidity
Enter fullscreen mode Exit fullscreen mode

Top comments (0)