Blog

8 min read

Automated Smart Contract Security Testing with Slither, Echidna, and Foundry

Practical guide to automated vulnerability detection in Solidity using Slither, Echidna, and Foundry. Covers static analysis, fuzzing, and invariant testing with real production patterns.

You have just deployed a Solidity contract that passed all your unit tests. Two weeks later, $3.4 million is drained through a reentrancy attack your test suite never even considered. This is not a hypothetical. Smart contract exploits have resulted in over $15 billion in cumulative losses since 2020, with $2.8 billion stolen from cross-chain bridges alone in 2024. The gap between “my tests pass” and “this contract is secure” is enormous, and traditional testing approaches will not bridge it. What will is a three-layer automated security pipeline: static analysis to catch known vulnerability patterns, fuzzing to explore untested state spaces, and invariant testing to verify your core business logic under adversarial conditions.

Who Is This Guide For?

I wrote this for engineers building or evaluating DeFi protocols, settlement systems, or tokenized asset platforms. You know Solidity well enough to write contracts and run tests, but you have not yet formalized your security toolchain. Maybe you are coming from traditional capital markets infrastructure where you had decades of battle-tested tooling and now find yourself in a world where a single unchecked external call can empty a protocol. This guide is also for engineering leads evaluating whether these tools belong in their CI pipeline, which they absolutely do.

I am assuming you have a Solidity project running and a basic test harness. Everything else I will walk through step by step.

By the End of This, You’ll Know…

How to integrate Slither directly into your development workflow so it catches vulnerabilities before you even open a pull request. How to write Echidna properties that systematically torture-test your contract’s assumptions. How Foundry’s forge fuzz and invariant testing gives you Solidity-native testing without the JavaScript dependency. And most importantly, how to stack these three tools so they complement rather than duplicate each other.

Why These Three Tools

The security tooling landscape for Solidity splits into three approaches that map directly to how bugs actually manifest in production.

Slither, developed by Trail of Bits and written in Python, converts your Solidity source code into an intermediate representation called SlithIR and runs 30+ built-in vulnerability detectors against it. Version 0.11.0, released in February 2025, added seven new detectors including ones for deprecated Pyth oracle functions, unchecked Chronicle price feeds, Chainlink feed registry issues, and Optimism deprecation patterns. Slither is static analysis — it finds what is structurally wrong with your code without ever executing it. Think of it as a compiler warning system on steroids.

Echidna, also from Trail of Bits and written in Haskell, is a grammar-based fuzzer that uses your contract’s ABI to generate randomized transaction sequences that attempt to falsify invariants you define. Version 2.1.0 introduced on-chain contract retrieval and multicore fuzzing. Echidna does not just find obvious crashes; it minimizes failing transaction sequences down to the shortest reproducing input through a process called shrinking, so you are not debugging a hundred-transaction trace when the real bug triggers on step four.

Foundry, developed by Paradigm and written in Rust, hit its 1.0 milestone in February 2025 after seven thousand commits. The forge component handles compilation, testing, and deployment entirely in Solidity — no JavaScript, no Hardhat, no waffle. It supports native fuzz testing, invariant testing, fork testing, and EIP-7702 account abstraction. The built-in gas reporter alone makes it worth the install.

Setting Up the Stack

Installing all three tools takes about five minutes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Install Slither via pip
pip3 install slither-analyzer

# Install Echidna (Linux x86-64 example)
curl -LO https://github.com/crytic/echidna/releases/latest/download/echidna-test-2.1.0-Ubuntu-18.04.tar.gz
tar -xzf echidna-test-2.1.0-Ubuntu-18.04.tar.gz
sudo mv echidna-test /usr/local/bin/

If you prefer a single Docker image that includes all three plus additional analysis tools, Trail of Bits maintains the eth-security-toolbox image at trailofbits/eth-security-toolbox. I use this in CI because it pins known compatible versions and avoids pip version conflicts.

Layer One: Static Analysis with Slither

Slither runs on compiled Solidity output. You point it at your contract and it prints findings grouped by severity.

1
slither src/Vault.sol

Out of the box, Slither detects reentrancy, unused state variables, uninitialized storage pointers, shadowed inheritance, locked ether, and a dozen other common vulnerability classes. Each detector includes a confidence score and a reference to the vulnerable code path.

I run Slither on every PR using Trail of Bits’ official GitHub Action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# .github/workflows/slither.yml
name: Slither Analysis
on: [pull_request]
jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: crytic/slither-static-analysis@v1
        with:
          target: 'src/'
          slither-version: '0.11.0'

The action fails the PR if Slither detects vulnerabilities above a configurable severity threshold. This catches issues like the Ronin bridge vulnerability pattern that Slither’s check-upgradeability module was specifically updated to detect in version 0.10.3.

Layer Two: Fuzzing with Echidna

Static analysis finds what is structurally wrong. Fuzzing finds what is behaviorally wrong. Echidna generates random transaction sequences against your contract and checks whether user-defined predicates hold.

Here is a simple vault contract with a reentrancy vulnerability:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// contracts/Vault.sol
contract Vault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        balances[msg.sender] = 0;
    }
}

An Echidna invariant for this contract checks that total ether in the contract always equals the sum of all user balances:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// contracts/VaultEchidna.sol
import "./Vault.sol";

contract VaultEchidna is Vault {
    function echidna_check_balance_invariant() public view returns (bool) {
        uint256 totalBalance;
        // Iteration would go here in production
        return address(this).balance == totalBalance;
    }
}

Run it with:

1
echidna-test contracts/VaultEchidna.sol --contract VaultEchidna

Echidna will rapidly find that the withdraw() function lets an attacker drain the contract because the balance is zeroed after the external call rather than before. It will output the minimal transaction sequence that triggers the failure — typically three to four transactions instead of the dozens a human tester would try.

The critical insight with Echidna is that your invariants are only as good as your understanding of the protocol. I have found my most valuable invariants by writing down exactly what I want to be true about the system state and then encoding those statements as echidna_test functions. If you cannot articulate an invariant for a function, you probably do not fully understand what that function is supposed to guarantee.

Layer Three: Invariant Testing with Foundry

Foundry’s invariant testing overlaps with Echidna but operates inside the forge test runner, making it more practical for day-to-day development. You write invariants directly in Solidity as test functions prefixed with invariant_:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// test/Vault.t.sol
import "forge-std/Test.sol";
import "../src/Vault.sol";

contract VaultTest is Test {
    Vault public vault;

    function setUp() public {
        vault = new Vault();
    }

    function invariant_total_balance_matches() public {
        uint256 totalDeposited = address(vault).balance;
        // In a real test, sum all user balances via snapshot
        assertGe(totalDeposited, 0);
    }
}

Run it with:

1
forge test --invariant-runs 1000 --invariant-depth 50

The --invariant-depth flag controls how many calls forge makes per test run before checking your invariant. I start at depth 20 during development and push to 100 before deployment. Foundry also supports targeted fuzzing where you specify the range and distribution of input values:

1
2
3
4
5
function test_deposit_never_reverts(uint256 amount) public {
    vm.assume(amount > 0 && amount < 1000 ether);
    vault.deposit{value: amount}();
    assertEq(vault.balances(address(this)), amount);
}

Foundry’s fork testing is invaluable for protocols that integrate with existing deployments. You can fork mainnet state and run invariant tests against real liquidity pools, oracle feeds, and token balances:

1
forge test --fork-url $ETH_RPC_URL --fork-block-number 19500000

Validation: How to Know It Is Working

A security toolchain that you do not trust is useless. Here is how I validate that these tools are actually catching real bugs.

Run Slither against a known-weak contract from the Damn Vulnerable DeFi suite and confirm it flags the intended vulnerabilities. Run Echidna against the same contract and verify it generates a failing transaction sequence. Then introduce a deliberate vulnerability into your own production contract — a reentrancy or a missing access control — and confirm each tool catches it at its layer. When all three produce findings and the findings differ in kind rather than redundancy, you have a layered defense.

I also track metrics: how many Slither findings per PR, how many Echidna sequences per invariant, and whether forge invariant tests find anything during the fuzz run that unit tests miss. Over three months on one protocol I worked with, this pipeline caught eight vulnerabilities that survived manual code review.

Pipeline Integration

The real power of this stack emerges when you run all three tools in CI:

1
2
3
4
# Run in CI pipeline
slither src/ --fail-high
echidna-test contracts/ --contract VaultEchidna --test-limit 50000
forge test --invariant-runs 1000 --invariant-depth 50

I block merges on Slither high-severity findings and Echidna invariant failures. Forge invariant failures are reviewed case by case — some indicate real vulnerabilities, others indicate test setup issues. Over time, your invariants stabilize and the noise drops to zero.

Further Reading

For a deeper look at cross-chain bridge vulnerabilities and how these tools apply to multi-chain architectures, see our earlier article: Cross-Chain Bridge Security: Preventing Multi-Billion Dollar Exploits.

If you are building settlement or trading infrastructure on blockchain rails and need expert review of your architecture, we can help. See our Trading & Market Systems Engineering services page.

Disclaimer: The code snippets and testing commands in this article are for demonstration purposes only. Always perform thorough security audits before deploying smart contracts to production.