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 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
# 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.jsonReading Slither Output
Slither output groups findings by impact and confidence. Understanding how to read this output is the first skill to develop:
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 triageTop Built-In Detectors — High Impact
// 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
| Detector | Impact | What It Catches | Example Finding |
|---|---|---|---|
| reentrancy-no-eth | Medium | CEI violation with non-ETH tokens | Token transfer before state update |
| missing-zero-check | Low | address param not checked for zero | owner = newOwner without null check |
| tx-origin | Medium | tx.origin used for auth | require(tx.origin == owner) |
| unchecked-lowlevel | Medium | .call() return value ignored | target.call(data) without checking bool |
| unchecked-send | Medium | .send() return value ignored | addr.send(amount) without check |
| timestamp | Low | block.timestamp used for RNG or critical logic | rand = block.timestamp % 100 |
| weak-prng | High | On-chain randomness from predictable sources | Using blockhash as random seed |
| locked-ether | Medium | Contract receives ETH but has no withdraw | payable function, no payable withdraw |
| divide-before-multiply | Medium | Integer division truncation order | (a/b) * c loses precision vs (a*c)/b |
| shadowing-local | Low | Local variable shadows state variable | uint owner = ... shadows address owner |
Filtering False Positives
// 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 flagSlither in CI
# .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
# 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() conditionsWhat Slither Misses — Limitations
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.
// 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)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
| Tool | Type | Best For | Limitation |
|---|---|---|---|
| Slither | Static analysis | Pattern-based bugs, quick triage | Misses logic bugs, has false positives |
| Mythril | Symbolic execution | Deep path exploration, integer issues | Slow, times out on complex contracts |
| Foundry Fuzz | Fuzzing | Edge cases in arithmetic, state machines | Needs good invariants defined manually |
| Foundry Invariant | Property testing | Protocol-level guarantees | Handler contracts needed, slow |
| Semgrep | Custom rules (regex-style) | Project-specific patterns, custom checks | Requires rule writing expertise |
| 4naly3er | Report generator | Gas optimizations, common findings list | Output needs human triage, many false positives |
| Manual Review | Human analysis | Business logic, economic attacks, composability | Slow, expensive, requires expertise |
# 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-summaryThe 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-analyzerand run withslither .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, andsuicidal— these catch entire categories of high-severity vulnerabilities automatically. - Suppress false positives with
// slither-disable-next-line detector-nameand always document why the suppression is justified. - Integrate Slither into CI with
crytic/slither-actionto 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.