We manage your security to weather the AI storm →

← Resource Hub

Smart Contract CI/CD Pipeline: Foundry, Slither and Fuzzing

By Owen Thurm Published 6 min read

Smart Contract CI/CD Pipeline: Foundry, Slither and Fuzzing

Quick answer. Run a GitHub Actions workflow on a pinned Foundry version that formats, builds, tests, fuzzes, runs Slither and diffs gas and storage layout on every pull request. Deploy through a multisig proposal, never a key stored in CI.

Watch: Smart Contract CI/CD Pipeline: Foundry, Slither and Fuzzing 1:41
Video transcript

Smart contract CI should test, fuzz and scan every pull request, then deploy through a multisig, never a key in CI.

Your pipeline is a target too. In March 2025, attackers repointed a GitHub action's tags to a commit that printed CI secrets into logs.

Run formatting, tests and static analysis on every pull request. Fuzz deeper nightly, check storage layout on proxy upgrades, and deploy only approved releases.

Pin every layer, or the pipeline cannot be audited: the Foundry release, actions by commit SHA, the Slither version, and a read-only token.

Foundry's CI example uses ten thousand fuzz runs, versus 256 in a default profile. Fix the seed on pull requests, and run longer campaigns nightly.

Slither fails on any finding by default. For code scanning alerts, set fail-on to none, upload the SARIF, and require results in a branch ruleset.

Never keep a deployer key in CI, where one compromised action could reach every secret. Let CI prepare transactions, and owners confirm them in a Safe.

GitHub keeps logs and artifacts 90 days by default, so archive release evidence yourself: versions, fuzz seeds, SARIF reports, storage layouts and approvals.

Guardian audits include invariant suites that can run in this pipeline, and our pentests test whether a compromised pipeline can sign for you. Read on, or talk to Guardian.

A smart contract repository needs a stricter pipeline than most software. Once code is live, a missed bug sits next to real funds.

The pipeline is attack surface too. In March 2025, attackers repointed version tags of the tj-actions/changed-files action to a malicious commit that printed CI secrets into workflow logs, GitHub's advisory says.

This template covers a Foundry project on GitHub Actions, with notes for Hardhat. Every command comes from the tool's own documentation.

Pipeline Stages

Split the work by what each stage protects and when it runs.

Stage When Tools
Format, lint, buildEvery pull requestforge fmt, forge lint, forge build
Tests and gasEvery pull requestforge test, forge snapshot, forge coverage
Static analysisEvery pull requestSlither, Aderyn
Deep fuzzingNightly and before releaseforge test with a CI profile
Upgrade checksAny change to a proxied contractforge inspect, OpenZeppelin validate
DeploymentApproved releases onlyforge script, GitHub environments, Safe

Pin everything

A pipeline that runs different tools each day cannot be audited. Pin each layer.

  • Foundry: foundry-toolchain installs stable unless version names a release.
  • Binaries: gh attestation verify checks a Foundry release's signed provenance.
  • Actions: GitHub recommends a workflow lockfile, or else full commit SHA pins.
  • Slither: set slither-version, or the action installs the latest release.
  • Token: give GITHUB_TOKEN the "minimum required permissions," starting read-only.

Format, Build and Test

This core job adapts Foundry's CI guide and the foundry-toolchain README. Each action is pinned to the commit behind its release tag.

name: contracts-ci
on: pull_request
permissions:
  contents: read
env:
  FOUNDRY_PROFILE: ci
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          submodules: recursive
          persist-credentials: false
      - uses: foundry-rs/foundry-toolchain@dba66c27052e63476d886f22216e119ba7e615bf # v1.9.2
        with:
          version: v1.9.0
      - run: forge fmt --check
      - run: forge lint --deny warnings
      - run: forge build --sizes
      - run: forge test -vvv
      - run: forge snapshot --check
  • fmt --check exits with 1 if any file needs formatting.
  • lint --deny warnings fails on any warning from Forge's linter.
  • snapshot --check fails if a test uses more gas than recorded.

The ci profile lives in foundry.toml, as in Foundry's guide.

Coverage

Add forge coverage --report lcov for a coverage service. Foundry "does not enforce a minimum percentage," so set thresholds there.

Coverage builds disable the optimizer by default, so they differ from production. Our mutation testing guide shows full line coverage still missing planted bugs.

Hardhat projects

Hardhat 3 runs Solidity tests, including fuzz tests, without a plugin. Its hardhat test --coverage writes LCOV and HTML reports.

Slither, Aderyn and SARIF

Slither is crytic's Solidity and Vyper static analyzer. Its GitHub Action can write SARIF, which code scanning shows as alerts.

  slither:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          submodules: recursive
          persist-credentials: false
      - uses: crytic/slither-action@b52cc1cbfee9ca3e8722dd5224299d16c9a6b80f # v0.4.2
        with:
          slither-version: 0.11.6
          sarif: results.sarif
          fail-on: none
      - uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
        with:
          sarif_file: results.sarif
  • Upload needs fail-on: none, the README says. The default fails on any finding.
  • Gate merges with a ruleset that requires code scanning results, with thresholds per tool.
  • Private repositories need GitHub Code Security, and the job needs actions: read.
  • Foundry version: the action installs Foundry stable itself. To keep your pin, build first and set ignore-compile.

Aderyn and forge lint

Aderyn is Cyfrin's Rust-based Solidity analyzer. It runs on Foundry and Hardhat projects without configuration and writes SARIF, among other formats.

Forge's own linter adds detectors such as unchecked calls. Record each waiver in the repository, such as a forge-lint: disable-next-line comment.

Fuzz and Invariant Jobs

Foundry's docs show 256 fuzz runs in a default profile. Their CI example raises that sharply:

[profile.ci.fuzz]
runs = 10000
seed = "0x1"

[profile.ci.invariant]
runs = 1000
depth = 1000
  • Pull requests: a fixed seed keeps a failure reproducible.
  • Nightly: run longer campaigns on a schedule, with more runs and depth.
  • Corpus: setting corpus_dir enables coverage-guided fuzzing that builds on earlier campaigns.
  • Failures: failure_persist_dir keeps counterexamples. Turn each into a regression test.
  • Fork tests: foundry-toolchain's cache does not work with fuzzed fork tests unless you set a seed.

Upgrades, Dry Runs and Approvals

Check storage layout on every upgrade

A proxy keeps its state across upgrades, so the new layout must stay compatible. Foundry's guide exports each version with forge inspect <contract> storage-layout and diffs them.

For a stricter gate, OpenZeppelin's upgrades-core validate checks upgrade safety and layout compatibility, and exits non-zero on errors.

Give it a reference contract with @custom:oz-upgrades-from or --reference. Without one, storage incompatibilities go unreported.

Then rehearse the upgrade on a fork. Our change management guide covers the release process around it.

Dry-run the deployment

Without --broadcast, Foundry's forge script only simulates: it writes a dry-run file and sends nothing. Upload that file for reviewers.

Approvals and signing

Put deployment jobs behind a GitHub environment with required reviewers. Per GitHub's docs:

  • Up to six users or teams can review; one approval releases the job.
  • An optional setting stops whoever started a deployment from approving it.
  • Environment secrets stay locked until a reviewer approves.
  • On Free, Pro and Team plans, required reviewers work only in public repositories.

Keep deployer keys out of CI secrets. In GitHub's words, one compromised action "would have access to all secrets configured on your repository."

  • Propose, don't sign. CI prepares the transactions; a signer proposes them to the Safe and the owners confirm.
  • Hardware wallets on mainnet, as Foundry's best practices advise.
  • If CI must sign, use a KMS key through OIDC, whose token is "only valid for a single job". Give it no admin role.

Verify on explorers

Foundry's forge script --verify verifies every contract in the script's receipts. Hardhat's hardhat verify covers Etherscan, Blockscout and Sourcify.

Then compare deployed bytecode with the release build, as our Foundry deployment guide shows.

Evidence Retention

After an incident, or in due diligence, someone will ask what ran before a release. Answer from records.

GitHub keeps artifacts and logs for 90 days by default. So copy release evidence to storage you control:

  • Commit SHA, tool versions and Foundry profile.
  • Test, fuzz and invariant results, with seeds and counterexamples.
  • SARIF reports with triage notes, LCOV and the gas diff.
  • Storage layouts for each upgraded contract.
  • Broadcast files, which record the Git commit they ran from.
  • Reviewer approvals and multisig transaction hashes.

NIST's Secure Software Development Framework (SP 800-218, version 1.1) recommends archiving each release with "integrity verification information" and "provenance data" (PS.3.1).

GitHub artifact attestations add signed provenance, so others can "verify where and how your software was built." For launch day, see our pre-mainnet security checklist.

How Guardian Helps

Guardian's smart contract audit includes invariant fuzzing: your core safety properties as executable invariants, "left runnable after the engagement." That suite can run in this pipeline.

An infrastructure pentest covers CI/CD runner permissions, artifact integrity "and whether a compromised pipeline can ship or sign on your behalf."

With Managed Security, Helix runs continuously across your stack. Only human-verified findings reach you, each with a fix PR attached.

Guardian's security operations team reviews every multisig transaction before it is signed, and every deployment and configuration change as it lands.

Frequently Asked Questions

How do I set up CI/CD for a Foundry project?

Install a pinned Foundry release with foundry-toolchain. On every pull request, check formatting, lint, build, test and compare gas. Add Slither with SARIF upload, a CI fuzz profile and a deployment job behind reviewers.

Should a deployer private key be stored in GitHub secrets?

No. GitHub warns that one compromised action can reach every repository secret. Let CI simulate and prepare transactions, and have signers approve them in a multisig from hardware wallets.

How many fuzz runs should CI use?

Foundry's CI example uses 10,000 fuzz runs, and 1,000 invariant runs at a depth of 1,000. Use a fixed seed on pull requests and run longer campaigns on a schedule.

Can Slither block a pull request?

Yes. The Slither action fails on any finding by default. To see findings as code scanning alerts, upload SARIF with fail-on set to none, then require code scanning results in a branch ruleset.

Sources

Ship every release with Guardian watching

Guardian reviews every multisig transaction before it is signed and every deployment as it lands, so your pipeline's output gets a second check.