🌿 Intermediate ⏱️ 35 min

Static Analysis with Slither

Static analysis is the practice of finding bugs without running code. Instead of executing a contract, static analysis tools parse the source code or bytecode and apply pattern-matching rules to detect known vulnerability classes. Slither, developed by Trail of Bits, is the industry standard for Solidity static analysis. It finds real bugs fast — in a mature codebase, running slither . for the first time typically surfaces 5-20 legitimate findings. This lesson covers Slither from installation through CI integration, with a complete breakdown of its most important detectors.

📖 Static vs Dynamic Analysis

Static analysis examines code structure without execution — it's fast, runs on every build, and catches predictable patterns like unchecked return values or missing zero-address checks. Dynamic analysis (fuzzing, symbolic execution) runs the code with specific or generated inputs — it finds bugs that require particular states but takes longer. Both approaches are complementary: use static analysis to eliminate low-hanging fruit, then apply dynamic analysis to the remaining logic.

Installing and Running Slither

📦 Installation and Basic Usage
# Install Slither via pip3 (requires Python 3.8+) pip3 install slither-analyzer # Verify installation slither --version # Run on a Foundry project (in the project root) slither . # Run on a Hardhat project slither . --hardhat-artifacts-directory artifacts # Run on a single file slither src/MyContract.sol # Run on a specific contract slither . --contract-name MyToken # Filter to specific detector(s) slither . --detect reentrancy-eth,unchecked-lowlevel # Exclude test files (important — tests intentionally do unsafe things) slither . --exclude-dependencies --filter-paths "test/" # Output as JSON (for CI pipelines) slither . --json slither-report.json

Reading Slither Output

Slither output groups findings by impact and confidence. Understanding how to read this output is the first skill to develop:

📋 Interpreting Slither Output
INFO:Detectors: Reentrancy in VulnerableBank.withdraw() (src/Bank.sol#45-54): External calls: - (ok) = msg.sender.call{value: amount}() (src/Bank.sol#50) State variables written after the call(s): - balances[msg.sender] = 0 (src/Bank.sol#52) Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities ┌────────────────────────────────────────────────────────────┐ │ Severity │ Impact │ Confidence │ Detector Name │ ├───────────┼────────────┼─────────────┼──────────────────────┤ │ HIGH │ High │ Medium │ reentrancy-eth │ │ HIGH │ High │ High │ unprotected-upgrade │ │ MEDIUM │ Medium │ High │ missing-zero-check │ │ LOW │ Low │ High │ events-maths │ │ INFO │ Informal │ High │ naming-convention │ └───────────┴────────────┴─────────────┴──────────────────────┘ Prioritize: High Impact + High Confidence first Review: High Impact + Medium Confidence (may be false positive) Skip: Low/Info findings in initial triage

Top Built-In Detectors — High Impact

🚨 Critical Detectors — What They Find
// reentrancy-eth: ETH transfer before state update function withdraw() external { msg.sender.call{value: balances[msg.sender]}(""); balances[msg.sender] = 0; // ← state write AFTER call — DETECTED } // unprotected-upgrade: upgradeTo callable by anyone function upgradeTo(address newImpl) external { // missing onlyOwner → DETECTED implementation = newImpl; } // arbitrary-send-eth: ETH sent to user-controlled address without restriction function sendFees(address to) external { payable(to).transfer(fees); // if 'to' is from user input → DETECTED } // controlled-delegatecall: delegatecall target from user input function execute(address target, bytes calldata data) external { target.delegatecall(data); // user controls target → DETECTED } // suicidal: selfdestruct callable by anyone function kill() external { selfdestruct(payable(msg.sender)); // no auth → DETECTED }

Medium and Low Impact Detectors

DetectorImpactWhat It CatchesExample Finding
reentrancy-no-ethMediumCEI violation with non-ETH tokensToken transfer before state update
missing-zero-checkLowaddress param not checked for zeroowner = newOwner without null check
tx-originMediumtx.origin used for authrequire(tx.origin == owner)
unchecked-lowlevelMedium.call() return value ignoredtarget.call(data) without checking bool
unchecked-sendMedium.send() return value ignoredaddr.send(amount) without check
timestampLowblock.timestamp used for RNG or critical logicrand = block.timestamp % 100
weak-prngHighOn-chain randomness from predictable sourcesUsing blockhash as random seed
locked-etherMediumContract receives ETH but has no withdrawpayable function, no payable withdraw
divide-before-multiplyMediumInteger division truncation order(a/b) * c loses precision vs (a*c)/b
shadowing-localLowLocal variable shadows state variableuint owner = ... shadows address owner

Filtering False Positives

🚫 Suppressing Slither Findings — When and How
// Method 1: disable a single line with inline comment function getTimestamp() external view returns (uint256) { return block.timestamp; // slither-disable-next-line timestamp } // Method 2: disable for a whole function // slither-disable-start reentrancy-eth function trustedWithdraw() external { // This function is safe because nonReentrant modifier is used } // slither-disable-end reentrancy-eth // Method 3: .slither.config.json — project-level configuration { "filter_paths": "test/,lib/,node_modules/", "exclude_informational": true, "detectors_to_exclude": "naming-convention" } // RULE: document WHY you're suppressing, not just that you are // An unexplained suppression in an audit is a red flag

Slither in CI

🧰 GitHub Actions CI Integration
# .github/workflows/slither.yml name: Slither Analysis on: [push, pull_request] jobs: analyze: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run Slither uses: crytic/slither-action@v0.3.0 id: slither with: target: '.' slither-args: '--exclude-dependencies --filter-paths "test/"' fail-on: high # fail CI on high-severity findings sarif: results.sarif - name: Upload SARIF results uses: github/codeql-action/upload-sarif@v2 with: sarif_file: results.sarif ## Alternative: direct pip install in workflow # - run: pip3 install slither-analyzer # - run: slither . --json slither-report.json --no-fail-pedantic # --no-fail-pedantic: exits 0 even with findings (inspect report separately)

Slither Printers — Beyond Bug Detection

📊 Slither Printers for Code Understanding
# Human-readable summary of all contracts slither . --print human-summary # Call graph — who calls what slither . --print call-graph # Outputs: mycontract.call-graph.dot (visualize with Graphviz) # Inheritance tree slither . --print inheritance slither . --print inheritance-graph # Function visibility — which functions are external? slither . --print function-summary # Variable access — which functions read/write which state variables? slither . --print vars-and-auth # Contract summary for quick audit triage slither . --print contract-summary # Detectors that are relevant only in specific contexts slither . --print modifiers # lists all modifiers and where they're used slither . --print require # lists all require() conditions

What Slither Misses — Limitations

🚨 Slither Does Not Replace Manual Review

Slither is a pattern matcher — it finds predictable, structural bugs. It cannot reason about business logic. It doesn't know if a fee calculation is correct, if an oracle is being manipulated, if a governance proposal is malicious, or if the protocol's economic invariants hold under adversarial conditions. Every Slither finding must be triaged by a human, and Slither's absence of findings does not mean a contract is safe.

⛔ Categories Slither Misses
// 1. Business logic bugs — requires understanding intent function calculateInterest(uint256 principal) public view returns (uint256) { return principal * interestRate / 10000; // is 10000 the right basis? Slither won't know } // 2. Oracle manipulation — requires DeFi context function getPrice() public view returns (uint256) { return uniswapPool.slot0().sqrtPriceX96; // spot price — manipulable // Slither won't flag this — it doesn't understand TWAP vs spot } // 3. Complex state dependencies // If contract X is safe when Y.state == Active but vulnerable when Y.state == Paused // Slither analyzes X in isolation — misses the cross-contract dependency // 4. Access control correctness // Slither can detect MISSING access control // It cannot detect WRONG access control (function guarded by MINTER_ROLE // when it should be ADMIN_ROLE)
💡 Document Your Suppressions in Audit Reports

When a project uses // slither-disable-next-line extensively, it's a sign that the development team understands the tool. When a suppression appears without a comment explaining why it's safe, treat it as a finding to investigate — the developer may have dismissed a real vulnerability as a false positive. In your audit reports, call out unexplained suppressions explicitly.

Combining Slither with Other Tools

ToolTypeBest ForLimitation
SlitherStatic analysisPattern-based bugs, quick triageMisses logic bugs, has false positives
MythrilSymbolic executionDeep path exploration, integer issuesSlow, times out on complex contracts
Foundry FuzzFuzzingEdge cases in arithmetic, state machinesNeeds good invariants defined manually
Foundry InvariantProperty testingProtocol-level guaranteesHandler contracts needed, slow
SemgrepCustom rules (regex-style)Project-specific patterns, custom checksRequires rule writing expertise
4naly3erReport generatorGas optimizations, common findings listOutput needs human triage, many false positives
Manual ReviewHuman analysisBusiness logic, economic attacks, composabilitySlow, expensive, requires expertise
✏️ Try It — Run Slither on a Known Vulnerable Contract
# Create a test vulnerable contract cat > /tmp/VulnerableBank.sol << 'EOF' pragma solidity ^0.8.0; contract VulnerableBank { mapping(address => uint256) public balances; function deposit() external payable { balances[msg.sender] += msg.value; } function withdraw() external { uint256 amount = balances[msg.sender]; require(amount > 0); msg.sender.call{value: amount}(""); // reentrancy! balances[msg.sender] = 0; } } EOF # Run Slither — should detect reentrancy-eth slither /tmp/VulnerableBank.sol # Expected output: # INFO:Detectors: # Reentrancy in VulnerableBank.withdraw() ... # Reference: reentrancy-vulnerabilities # Run on your own project: cd /your/foundry/project slither . --exclude-dependencies --filter-paths "test/" --print human-summary
💡 Audit Workflow — Slither First, Manual Second

The most efficient audit workflow: run Slither first to eliminate all the mechanical bugs (unchecked returns, missing zero checks, obvious reentrancy). Document each finding and either confirm it's a real bug or dismiss it with reasoning. Then spend your expensive manual review time on protocol logic, economic invariants, cross-contract interactions, and access control correctness — areas where tools systematically fail. This gives you the best bugs-per-hour ratio.

Key Takeaways

  • Slither is a static analysis tool — it finds bugs by pattern matching against source code without executing it. Install with pip3 install slither-analyzer and run with slither . in your project root.
  • Findings are organized by impact (High/Medium/Low/Info) and confidence (High/Medium/Low). Prioritize High Impact + High Confidence findings first in triage.
  • Critical detectors include reentrancy-eth, unprotected-upgrade, controlled-delegatecall, arbitrary-send-eth, and suicidal — these catch entire categories of high-severity vulnerabilities automatically.
  • Suppress false positives with // slither-disable-next-line detector-name and always document why the suppression is justified.
  • Integrate Slither into CI with crytic/slither-action to catch regressions before they reach deployment.
  • Slither misses business logic bugs, oracle manipulation, economic attack vectors, and cross-contract state dependencies — always follow static analysis with thorough manual review focused on these areas.