🌳 Advanced ⏱️ 45 min

Tools Workflow: Slither + Foundry + Echidna

A professional auditor's toolbox is carefully chosen and deeply practiced. Using the right tool for the right phase of an audit multiplies your output dramatically — but using tools randomly, without a workflow, wastes time and creates false confidence. This lesson presents the complete four-phase tooling workflow used by top competitive auditors, with specific commands, configurations, and time allocations for a 7-day contest audit.

📖 Tool Philosophy

Automated tools find the bugs that can be found automatically. These are almost never the highest-value findings in a competitive audit — those require human understanding of protocol logic. Use tools to clear the "known pattern" backlog quickly so your manual review time focuses on the logic bugs that tools cannot find. Tools should take ~30% of your time; human review should take ~50%; PoC writing should take ~20%.

Phase 1: First Pass with Slither

🔍 Slither Installation and Setup
# Install Slither and its dependencies $ pip3 install slither-analyzer $ pip3 install crytic-compile # For Foundry-based repos (most common) $ cd target-repo && forge build $ slither . --foundry-compile-all # For Hardhat repos $ npm install && npx hardhat compile $ slither . # Step 1: Architecture overview BEFORE looking for bugs $ slither . --print human-summary # Output: List of all contracts, functions, variables # Shows: which functions modify state, which are view # Step 2: Call graph — who calls what $ slither . --print call-graph $ dot -Tpng call-graph.dot -o call-graph.png # Step 3: Inheritance diagram — understand contract hierarchy $ slither . --print inheritance-graph # Step 4: Full detector run, save output $ slither . --json slither-output.json 2>&1 | tee slither-report.txt # Step 5: Filter to High/Medium only to reduce noise $ slither . --exclude-low --exclude-informational --exclude-optimization
🔍 Reading Slither Output: True vs False Positives
// Slither will report many findings. Not all are valid. // High true-positive rate detectors (always investigate): // - reentrancy-eth, reentrancy-no-eth // - unchecked-transfer (SafeERC20 not used) // - arbitrary-send-eth // - controlled-delegatecall // - suicidal (selfdestruct by anyone) // - uninitialized-state-variable // Common false positives (investigate but often not valid): // - reentrancy-benign: flags nonReentrant functions incorrectly sometimes // - divide-before-multiply: flags correct precision scaling // - tautology: flags intentional defensive checks // Mark false positives with Slither's ignore comments: // // slither-disable-next-line divide-before-multiply uint256 result = (a / PRECISION) * b; // intentional scaling

Phase 2: Manual Review with Foundry

🔍 Setting Up a Fork Test Environment
// foundry.toml — fork configuration [profile.default] src = "src" out = "out" libs = ["lib"] [rpc_endpoints] mainnet = "${ETH_RPC_URL}" // Alchemy or Infura arbitrum = "${ARB_RPC_URL}" polygon = "${POLYGON_RPC_URL}" [etherscan] mainnet = { key = "${ETHERSCAN_KEY}" } // Fork test template for investigating a suspicious function contract InvestigationTest is Test { IProtocol target; function setUp() public { // Fork mainnet at a recent block vm.createSelectFork("mainnet"); // Point to deployed protocol target = IProtocol(0x1234...); // Label addresses for readable traces vm.label(address(target), "TargetProtocol"); vm.label(0xA0b8..., "USDC"); } // Run with: forge test --match-test testInvestigate -vvvv // -vvvv shows full call traces including reverts }
🔍 Key Foundry Cheatcodes for Exploit Development
// vm.prank(address) — next call comes from this address vm.prank(attacker); vault.withdraw(1000e18); // executes as attacker // vm.startPrank(address) / vm.stopPrank() — multiple calls as same address vm.startPrank(attacker); token.approve(address(vault), type(uint256).max); vault.deposit(1000e18); vm.stopPrank(); // vm.deal(address, amount) — set ETH balance vm.deal(attacker, 100 ether); // deal(token, address, amount) — set ERC20 balance deal(address(USDC), attacker, 1_000_000e6); // 1M USDC // vm.warp(timestamp) — set block.timestamp vm.warp(block.timestamp + 7 days); // skip 7 days // vm.roll(blockNumber) — set block.number vm.roll(block.number + 100); // advance 100 blocks // vm.expectRevert() — assert next call reverts with specific message vm.expectRevert("Insufficient balance"); vault.withdraw(1000e18); // vm.expectEmit() — assert next call emits specific event vm.expectEmit(true, true, false, true); emit Withdrawn(attacker, 1000e18); vault.withdraw(1000e18); // vm.load / vm.store — direct storage manipulation for testing // Read slot 0 of a contract bytes32 slot0 = vm.load(address(vault), bytes32(uint256(0))); // Override: set the owner slot directly vm.store(address(vault), ownerSlot, bytes32(uint256(uint160(attacker))));

Phase 3: Fuzzing with Echidna and Foundry Invariant Tests

🔍 Foundry Invariant Testing
// Foundry invariant test — runs random sequences of function calls // and checks that your invariant holds after every call contract VaultInvariantTest is Test { Vault vault; Handler handler; function setUp() public { vault = new Vault(); handler = new Handler(vault); // Only fuzz calls to the handler — handler calls vault targetContract(address(handler)); } // Invariant: total assets always >= total shares (no share inflation) function invariant_totalAssets_geq_totalShares() public { assertGe( vault.totalAssets(), vault.totalSupply(), "Invariant violated: totalAssets < totalSupply" ); } // Invariant: no user can withdraw more than they deposited (ignoring yield) function invariant_noTheft() public { for (uint256 i; i < handler.actorCount(); i++) { address actor = handler.actors(i); assertLe( handler.withdrawnByActor(actor), handler.depositedByActor(actor) + vault.earned(actor), "Actor withdrew more than deposited + earned" ); } } } // Run: forge test --match-contract VaultInvariantTest // Configure: [invariant] in foundry.toml // runs = 1000 (number of random sequences) // depth = 100 (max calls per sequence)
🔍 Echidna: Property-Based Fuzzing
// echidna-test.sol — Echidna property file pragma solidity ^0.8.19; import "./Vault.sol"; contract EchidnaVaultTest { Vault vault; address[] actors; constructor() { vault = new Vault(); actors.push(address(0x1)); actors.push(address(0x2)); } // Echidna tests all functions named echidna_* as invariants // Returns true if invariant holds, false if violated function echidna_balance_never_negative() public view returns (bool) { return address(vault).balance >= 0; // Always true for uint } function echidna_totalShares_matches_sum() public view returns (bool) { uint256 sum = 0; for (uint256 i; i < actors.length; i++) { sum += vault.balanceOf(actors[i]); } return sum == vault.totalSupply(); } } // Run: echidna . --contract EchidnaVaultTest --config echidna.yaml // echidna.yaml // testLimit: 50000 // seqLen: 100 // coverage: true

Supporting Tools Reference

ToolPurposeWhen to UseInstall Command
SlitherStatic analysis, pattern detectionPhase 1 — every auditpip3 install slither-analyzer
FoundryTesting, fork tests, PoC writingPhase 2 — every auditcurl -L foundry.paradigm.xyz | bash
EchidnaProperty-based fuzzingPhase 3 — complex state machinesgithub.com/crytic/echidna releases
4naly3erAutomated report generationPhase 1 — quick first-pass reportnpx 4naly3er src/
aderynRust-based static analysisPhase 1 — run alongside Slithercargo install aderyn
TenderlyTransaction simulation, debuggingReproducing mainnet transactionstenderly.co (web-based)
SemgrepCustom pattern matchingChecking a specific pattern across large codebasepip3 install semgrep
HalmosSymbolic executionPhase 4 — proving properties exhaustivelypip3 install halmos

Full 7-Day Audit Workflow

🔍 Time Allocation for a 7-Day Contest
// Day 1: Context and architecture (8 hours) // Hours 1-2: Read README, docs, prior audits // Hours 3-4: Run Slither printers (human-summary, call-graph, inheritance) // Hours 5-6: Read test suite, note coverage gaps // Hours 7-8: List all entry points, trace asset flows, draft threat model // Day 2: Automated analysis (6 hours) // Hours 1-2: Full Slither detector run, triage findings // Hours 3-4: 4naly3er and aderyn, note non-overlapping findings // Hours 5-6: Run existing test suite, check for failures // Day 3-4: Deep manual review (16 hours) // Focus on: high-value functions, coverage gaps, suspicious Slither flags // Use: threat model to guide priorities // Document: every suspicious finding in notes // Day 5-6: PoC development and fuzzing (14 hours) // Hours 1-4: Write invariant fuzz tests for core protocol properties // Hours 5-10: Write PoC tests for each lead from manual review // Hours 11-14: Investigate any invariant violations found by fuzzing // Day 7: Report writing and finalization (8 hours) // Hours 1-4: Write all finding reports (Title through Mitigation) // Hours 5-6: Review all findings — are severities correct? // Hours 7-8: Final checklist pass, submit // Time allocation summary: // Tools/automation: ~30% (17 hours) // Manual review: ~50% (27 hours) // PoC + reports: ~20% (11 hours)
💡 Maximizing Findings in Competitive Audits

In a competitive audit, spending 6 days on one potential Critical is less valuable than finding 3 Highs and 5 Mediums. Build a triage habit: spend no more than 2 hours investigating any single lead before deciding to either escalate it (write the PoC now) or flag it for later and move on. The lowest-hanging fruit is often found by methodically checking the same pattern across all functions, not by going deep on one function early.

✏️ Try It: Your Personal Audit Script
#!/bin/bash # audit-init.sh — run at start of every new audit REPO_PATH=$1 cd $REPO_PATH ## 1. Build the project forge build 2&>1 | tail -5 ## 2. Run existing tests forge test --summary 2&>1 | tail -10 ## 3. Coverage report forge coverage --report summary 2&>1 | grep -E "sol|%" ## 4. Slither — architecture first slither . --print human-summary 2&>1 | tee slither-summary.txt ## 5. Slither — full detector run slither . --exclude-low --exclude-informational --json slither.json 2&>1 | tee slither-findings.txt ## 6. Count external functions (entry points) grep -r "function.*external" src/ | grep -v "view\|pure" | wc -l ## 7. Open everything for review echo "Setup complete. External function count above. Start with slither-findings.txt"

Tenderly: Transaction Simulation for Live Protocols

🔍 Using Tenderly to Debug and Prove Exploits
// Tenderly lets you simulate transactions against mainnet state // Essential for: reproducing past exploits, testing fixes on live contracts // Use case 1: Replay a mainnet transaction to understand it // Go to Tenderly.co → Transaction Simulator // Paste the exploit transaction hash // Tenderly shows: full call trace, state changes, emitted events // Much more readable than raw Etherscan trace // Use case 2: Test your exploit PoC on mainnet before writing the Foundry test // Fork mainnet at latest block // Deploy your attacker contract in simulation // Execute the attack — see if it works and what the exact outcome is // Use the output to write the Foundry test accurately // Use case 3: Verify a protocol fix works // Simulate applying the patch to the live contract // Re-run the exploit — does it now revert? // Tenderly CLI (for automation): $ npm install -g @tenderly/cli $ tenderly login $ tenderly simulate tx 0xabc... --network mainnet // Foundry's built-in debug output (alternative to Tenderly): $ forge test --match-test testExploit -vvvv 2>&1 | head -200 // -vvvv shows: call depth, function calls, storage reads/writes // This is usually sufficient for PoC development without Tenderly

Semgrep: Custom Pattern Detection

🔍 Writing a Custom Semgrep Rule
# Custom Semgrep rule: detect missing Chainlink staleness check # File: rules/chainlink-staleness.yml rules: - id: missing-chainlink-staleness-check patterns: - pattern: | (, $PRICE, , , ) = $FEED.latestRoundData(); ... return $PRICE; - pattern-not: | (, $PRICE, , $UPDATED_AT, ) = $FEED.latestRoundData(); require(block.timestamp - $UPDATED_AT <= $THRESHOLD); message: > Chainlink latestRoundData() result used without staleness check. Price data could be hours old. severity: WARNING languages: [solidity] # Run against target codebase: $ semgrep --config rules/chainlink-staleness.yml src/ // More useful custom rules to write: // - ecrecover without address(0) check // - transfer() without return value check (non-SafeERC20 usage) // - block.timestamp used for randomness // - delegatecall to user-controlled address // - tx.origin authentication

Building Your Personal Toolkit

Script / TemplatePurposeTime to Build
audit-init.shOne command to clone, build, run Slither, generate coverage — run at start of every audit30 minutes
finding-template.mdPre-filled report template with all 7 sections — never start from blank15 minutes
InvariantTest.t.sol templateHandler + invariant test skeleton for common protocol types1-2 hours
ForkTest.t.sol templatePre-configured fork test with labeling and common deal() calls30 minutes
personal-checklist.mdYour accumulated checklist from all audits — add one item per missed findingOngoing
Custom Slither detectorsProtocol-pattern-specific detectors you write after finding recurring issues2-4 hours each
Custom Semgrep rulesYAML rules for patterns you check manually but could automate30 min each

Triage Workflow: From Raw Tool Output to Investigation Leads</html2>

Raw tool output is not actionable — it requires triage to separate genuine findings from false positives and low-priority noise. A structured triage workflow prevents the common mistake of spending hours investigating a Slither false positive while missing a critical logic bug.

🔍 Structured Tool Output Triage
// Step 1: Categorize each finding as Investigate / Likely FP / Known Issue $ slither . 2>&1 | tee slither-raw.txt $ wc -l slither-raw.txt # How big is the output? // Step 2: Sort by severity — focus on High before Medium before Low $ slither . --json slither-output.json 2>/dev/null $ cat slither-output.json | jq '.results.detectors[] | select(.impact=="High") | .check' // Step 3: False positive quick-filter — these are usually FP // - "reentrancy-no-eth" on pure accounting functions (no ETH transfer) // - "arbitrary-send-eth" on functions with access control // - "controlled-array-length" on functions that only the owner can call // - "tx-origin" on functions that correctly check msg.sender first // Step 4: Genuine Slither findings worth investigating (not FP) // - "reentrancy-eth" on public functions that send ETH // - "uninitialized-local" — can indicate missing initialization bug // - "incorrect-equality" — dangerous strict equality check (block.number ==, balance ==) // - "suicidal" — delegatecall + selfdestruct combination // - "unchecked-lowlevel" — .call() without checking return value // Step 5: Cross-reference with 4naly3er output // 4naly3er often finds different patterns than Slither // Findings in BOTH tools = high confidence it is real, investigate first // Findings in only one tool = still investigate, but check the other first // Step 6: Build investigation queue // Format: [tool] [finding] [file:line] [triage status] // [slither] reentrancy-eth Vault.sol:145 INVESTIGATE // [4naly3er] missing-events Protocol.sol:89 LOW PRIORITY // [slither] arbitrary-send-eth Owner.sol:45 LIKELY FP (onlyOwner)
💡 When Tools Contradict Your Analysis

Sometimes Slither flags code that you have manually verified as safe. Do not simply mark it as a false positive — understand WHY Slither flags it. Slither uses pattern matching and may be overly conservative. But occasionally, Slither's pattern matching reveals a subtlety you missed in manual review. When in doubt: write a test that exercises the flagged code path and confirm safety programmatically. "I looked at it and it seems fine" is not a valid reason to dismiss a tool finding — "I wrote a test that proves it cannot be exploited" is.

Key Takeaways

  • Tools should consume 30% of audit time — not 80% and not 0%. They clear the known-pattern backlog so human time focuses on logic bugs.
  • Slither's printers (human-summary, call-graph, inheritance) provide architectural understanding faster than reading code — always run these first.
  • Foundry's cheatcodes (vm.prank, vm.deal, vm.warp, vm.store) give you surgical control for reproducing and proving vulnerabilities in fork tests.
  • Invariant fuzzing with Foundry or Echidna systematically finds property violations that manual review misses — write invariants before you start manual review.
  • Tenderly's transaction simulation lets you reproduce mainnet exploits and test fixes without deploying contracts — use it when Foundry traces are insufficient.
  • Custom Semgrep rules automate recurring manual checks — each rule you write saves hours across future audits.
  • The 7-day time allocation (30% tools, 50% manual, 20% PoC+reports) is a starting point — adjust based on codebase size and complexity.
  • Build a personal audit starter script that runs your standard tool suite automatically on any new codebase — consistency beats heroics.