All articles
Guides·May 2, 2026·5 min read

EVM Audit Checklist: What Foundry and Slither Actually Test

An EVM smart contract audit checklist: what Foundry fuzzing and Slither actually catch, and where manual review still has to close the gap.

What the tools are actually for

Foundry and Slither get name-dropped in every audit report's methodology section, but most people outside the field have no idea what each one actually catches versus what still needs a human staring at the bytecode logic for three days. They solve different problems. Slither is a static analyzer — it reads your Solidity source and AST without executing anything, and flags patterns that match known bug classes. Foundry is a testing framework — it compiles and runs your contracts, and with forge fuzz it throws thousands of randomized inputs at your functions to find inputs that break your invariants. One tells you "this pattern looks dangerous." The other tells you "this specific sequence of calls actually breaks your accounting." An EVM smart contract audit checklist that only uses one of these is incomplete, and a checklist that stops at "ran the tools" instead of doing manual review is worse than incomplete — it's a false sense of coverage.

What Slither actually flags

Slither ships around 90 detectors, and in practice maybe 15 of them matter on a typical DeFi codebase. The ones that consistently earn their keep:

  • Reentrancy (reentrancy-eth, reentrancy-no-eth) — flags external calls followed by state writes, the classic checks-effects-interactions violation. Catches maybe 70% of real reentrancy paths; misses cross-function and cross-contract reentrancy where the vulnerable state lives in a different contract than the external call.
  • Unchecked low-level calls.call() and .send() return values that get silently dropped.
  • Uninitialized storage/state variables — common in upgradeable proxy patterns where a constructor runs but an initializer doesn't, or runs twice.
  • Arbitrary delegatecall to user-controlled addresses — this alone has caused eight-figure losses.
  • tx.origin used for authorization — trivially phishable via a malicious contract in the call chain.
  • Incorrect ERC20 return value handling — some tokens (USDT included) don't return a bool on transfer, and code that assumes they do silently succeeds on failed transfers.

Run it with slither . --exclude-informational --exclude-low to cut noise, then triage every medium-and-up finding by hand. Roughly a third of Slither's "high" findings on a mature codebase are false positives once you understand the actual call context — this is expected, not a sign the tool is broken.

What Foundry fuzzing actually tests

Fuzzing is where you verify economic and mathematical invariants rather than syntactic patterns. A fuzz test doesn't know what a reentrancy bug looks like — it just knows that after any sequence of valid operations, some property should still hold. Example: a lending pool's total shares should never exceed total deposited assets after arbitrary deposit/withdraw/liquidate sequences.

function invariant_totalSharesNeverExceedAssets() public {
    uint256 totalShares = pool.totalSupply();
    uint256 totalAssets = pool.totalAssets();
    assertLe(totalShares, totalAssets + ACCEPTABLE_ROUNDING_DUST);
}

function testFuzz_withdrawNeverExceedsDeposit(uint256 depositAmt, uint256 withdrawAmt) public {
    depositAmt = bound(depositAmt, 1, 1e30);
    withdrawAmt = bound(withdrawAmt, 0, depositAmt);

    vm.startPrank(user);
    token.approve(address(pool), depositAmt);
    pool.deposit(depositAmt);
    uint256 balBefore = token.balanceOf(user);
    pool.withdraw(withdrawAmt);
    uint256 balAfter = token.balanceOf(user);
    vm.stopPrank();

    assertEq(balAfter - balBefore, withdrawAmt);
}

The bound() calls matter more than they look — unbounded fuzz inputs waste runs on values like type(uint256).max that revert immediately on overflow checks and never reach the interesting logic. Foundry's invariant_ tests go further: they run a stateful sequence of random function calls (deposit, borrow, liquidate, repay, in random order and random amounts) across a configurable number of runs and depth, then check the invariant holds after every single call in the sequence, not just at the end. Set runs = 1000 and depth = 50 minimum for anything handling real value; the default config is too shallow to find multi-step exploits like the kind that drain a vault through three specific calls in sequence.

Fuzzing is exceptional at rounding errors, integer overflow edge cases (even with Solidity 0.8's built-in checks, unchecked blocks still exist), and share-price manipulation in vault-style contracts. It is bad at anything involving multi-block time manipulation, oracle price staleness, or cross-contract MEV — those need scenario-specific tests you write by hand, not generic invariants.

Where both tools go blind

Neither tool understands your business logic is wrong. Slither won't tell you that your liquidation threshold math lets a position get liquidated before it's actually undercollateralized. Foundry won't invent the invariant "the DAO's timelock should never be bypassable by a single signer" unless you write that test yourself — and if you don't know to write it, the gap stays invisible. Access control logic that's syntactically correct but semantically wrong (a role that should require 2-of-3 multisig but only checks onlyOwner) passes both tools clean.

This is also where centralization risk lives — upgrade keys, admin functions that can drain fees or pause withdrawals selectively, oracle feeds with a single point of failure. None of that trips a static analyzer. It requires someone reading the contract with the mental model of "if I were malicious and had this exact permission set, what's the worst thing I could do," which is precisely the manual review phase that separates a real audit from a tool report with a PDF wrapper around it.

If you're comparing quotes, our breakdown of smart contract audit cost for EVM, Solana, and TON in 2026 covers what actually drives price — LOC, external call surface, and whether the protocol is a fork or novel logic. The same tool-plus-manual-review split applies outside EVM too, if you're also shipping on TON: see what we check in a TON smart contract audit for FunC and Tact and the specific pitfalls in Jetton and NFT standard implementations.

A minimal pre-audit checklist

Before you even bring in outside eyes, run this yourself:

  1. slither . with default detectors, triage every high/medium finding
  2. Invariant fuzz tests for every state-changing function that moves value, minimum 1000 runs
  3. 100% branch coverage on access control (forge coverage), not just line coverage
  4. Manual trace of every delegatecall, selfdestruct, and admin-only function for who can call it and what happens if they do
  5. Check every external token interaction against the actual token contract, not an idealized ERC20 assumption

Teams that run this before a formal audit consistently get shorter, cheaper engagements, because the auditor isn't spending billable hours finding things a five-minute slither run would've caught.

If you want a second set of eyes that goes past tool output — including the access control and economic logic review that Slither and Foundry structurally can't do — our smart contract audit service covers exactly this gap, and for teams still shaping the contract architecture, a strategy consultation before you write the first line of Solidity is usually cheaper than fixing it after.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#EVM audit#Foundry#Slither#smart contract security#solidity