Zum Hauptinhalt springen
BlockchainMar 28, 2026

Deep EVM #20: CI/CD for Smart Contracts — Testing, Gas Regression, and Safety

OS
Open Soft Team

Engineering Team

Why Smart Contracts Need Their Own CI/CD

Smart contracts are not like regular software. Once deployed, they are immutable. A bug in a web app means a hotfix and a redeployment. A bug in a smart contract means lost funds, permanent state corruption, or a costly proxy upgrade. This changes the entire CI/CD philosophy: the pipeline is not just about catching regressions — it is the last line of defense before irreversible deployment.

A production-grade smart contract CI/CD pipeline must include: compilation and type checking, comprehensive test suites (unit, fuzz, invariant), gas regression tracking, static analysis for vulnerability patterns, deployment simulation, and automated contract verification.

Pipeline Architecture

Here is a complete GitHub Actions workflow for a Foundry-based smart contract project:

# .github/workflows/ci.yml
name: Smart Contract CI

on:
  push:
    branches: [main]
  pull_request:

env:
  FOUNDRY_PROFILE: ci

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - uses: foundry-rs/foundry-toolchain@v1
        with:
          version: nightly

      - name: Install Huff
        run: |
          curl -L get.huff.sh | bash
          echo "$HOME/.huff/bin" >> $GITHUB_PATH

      - name: Build
        run: forge build --sizes

      - name: Check contract sizes
        run: |
          forge build --sizes 2>&1 | while read line; do
            size=$(echo "$line" | grep -oP '\d+\.\d+' | head -1)
            if [ ! -z "$size" ] && (( $(echo "$size > 24.0" | bc -l) )); then
              echo "::error::Contract exceeds 24KB limit: $line"
              exit 1
            fi
          done

  test:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: foundry-rs/foundry-toolchain@v1

      - name: Unit tests
        run: forge test --match-path "test/unit/*" -vvv

      - name: Fuzz tests
        run: |
          forge test --match-path "test/fuzz/*" \
            --fuzz-runs 50000 -vv

      - name: Invariant tests
        run: |
          forge test --match-path "test/invariant/*" \
            --invariant-runs 512 \
            --invariant-depth 256 -vv

      - name: Fork tests
        run: |
          forge test --match-path "test/fork/*" \
            --fork-url ${{ secrets.ETH_RPC_URL }} -vvv
        env:
          ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}

  gas:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: foundry-rs/foundry-toolchain@v1

      - name: Gas snapshot
        run: forge snapshot

      - name: Compare gas
        run: forge snapshot --check --tolerance 2

      - name: Upload snapshot
        uses: actions/upload-artifact@v4
        with:
          name: gas-snapshot
          path: .gas-snapshot

  static-analysis:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Slither analysis
        uses: crytic/slither-action@v0.4.0
        with:
          sarif: results.sarif
          fail-on: medium

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

      - name: Aderyn analysis
        run: |
          cargo install aderyn
          aderyn . --output report.md

      - name: Upload Aderyn report
        uses: actions/upload-artifact@v4
        with:
          name: aderyn-report
          path: report.md

Gas Regression Tracking with forge snapshot

Gas regression is a critical metric for smart contracts. A 5% gas increase in a DEX router’s swap function costs real money at scale. Track it systematically:

# Generate baseline (commit to repo)
forge snapshot > .gas-snapshot
git add .gas-snapshot
git commit -m "chore: update gas snapshot"

# In CI, compare against baseline
forge snapshot --check .gas-snapshot --tolerance 2

The --tolerance 2 flag allows up to 2% gas increase before failing. For hot paths like MEV bot operations, set tolerance to 0%.

For PR comments showing gas diffs, create a custom action:

  gas-report:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: foundry-rs/foundry-toolchain@v1

      - name: Generate gas diff
        run: |
          forge snapshot --diff .gas-snapshot > gas-diff.txt 2>&1 || true
          echo "## Gas Report" > comment.md
          echo '```' >> comment.md
          cat gas-diff.txt >> comment.md
          echo '```' >> comment.md

      - name: Post PR comment
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          path: comment.md

Static Analysis: Slither and Aderyn

Slither

Slither is the industry-standard static analyzer for Solidity. It detects vulnerability patterns like reentrancy, unchecked external calls, and access control issues:

slither . --filter-paths "test/|script/|lib/" \
  --exclude naming-convention,solc-version \
  --fail-on medium

Common findings and how to handle them:

FindingSeverityAction
ReentrancyHighFix immediately, use CEI pattern
Unchecked returnMediumAdd return value check
Naming conventionInfoSuppress with --exclude
Unused state variableLowRemove or suppress

For Huff contracts, Slither cannot analyze the bytecode directly. Instead, analyze the Solidity interface contracts and integration tests that call the Huff contracts.

Aderyn

Aderyn is a Rust-based static analyzer that complements Slither with additional checks:

aderyn . --output report.json --format json

Aderyn is particularly good at catching gas optimization opportunities and detecting centralization risks.

Deployment Checklists

Automate pre-deployment checks with a script:

// script/DeployChecklist.s.sol
contract DeployChecklist is Script {
    function run() external view {
        // 1. Verify compiler settings
        require(block.chainid == 1, "Wrong chain!");

        // 2. Verify constructor args
        address expectedOwner = vm.envAddress("OWNER");
        require(expectedOwner != address(0), "Owner not set");

        // 3. Verify all dependencies are deployed
        require(
            IWETH(WETH).totalSupply() > 0,
            "WETH not deployed"
        );

        console.log("All checks passed");
    }
}
# Dry run against mainnet fork
forge script script/DeployChecklist.s.sol \
  --fork-url $ETH_RPC_URL -vvv

Automated Verification on Etherscan

Post-deployment, automatically verify contracts on Etherscan so users can read the source:

forge verify-contract \
  --chain mainnet \
  --num-of-optimizations 200 \
  --watch \
  --compiler-version v0.8.24 \
  --etherscan-api-key $ETHERSCAN_KEY \
  $CONTRACT_ADDRESS \
  src/MyContract.sol:MyContract

For contracts with constructor arguments:

# Encode constructor args
cast abi-encode "constructor(address,uint256)" $OWNER $INITIAL_SUPPLY

# Pass encoded args to verify
forge verify-contract \
  --constructor-args $(cast abi-encode "constructor(address,uint256)" $OWNER $INITIAL_SUPPLY) \
  $CONTRACT_ADDRESS \
  src/MyContract.sol:MyContract

Automate this in CI by adding a deployment job that triggers on tag pushes:

  deploy-verify:
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [test, gas, static-analysis]
    environment: mainnet
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: foundry-rs/foundry-toolchain@v1

      - name: Deploy
        run: |
          forge script script/Deploy.s.sol \
            --rpc-url ${{ secrets.ETH_RPC_URL }} \
            --broadcast \
            --verify \
            --etherscan-api-key ${{ secrets.ETHERSCAN_KEY }} \
            -vvv
        env:
          PRIVATE_KEY: ${{ secrets.DEPLOYER_KEY }}

Deployment Simulation

Before deploying to mainnet, simulate the entire deployment against a fork:

# Simulate deployment
forge script script/Deploy.s.sol \
  --fork-url $ETH_RPC_URL \
  --sender $DEPLOYER \
  -vvvv

# If simulation passes, broadcast
forge script script/Deploy.s.sol \
  --rpc-url $ETH_RPC_URL \
  --broadcast \
  --slow \
  --verify

The --slow flag submits transactions one at a time and waits for confirmation, which is safer for multi-step deployments.

Monitoring Post-Deployment

After deployment, set up automated monitoring:

  post-deploy-check:
    runs-on: ubuntu-latest
    needs: deploy-verify
    steps:
      - name: Verify deployment
        run: |
          # Check contract is verified on Etherscan
          STATUS=$(curl -s "https://api.etherscan.io/api?module=contract&action=getabi&address=$CONTRACT" | jq -r '.status')
          [ "$STATUS" = "1" ] || exit 1

          # Check contract responds correctly
          OWNER=$(cast call $CONTRACT "owner()" --rpc-url $RPC)
          [ "$OWNER" = "$EXPECTED_OWNER" ] || exit 1

          echo "Deployment verified successfully"

Security Tooling Summary

ToolTypeBest For
SlitherStatic analysisVulnerability patterns, Solidity
AderynStatic analysisGas, centralization risks
Foundry FuzzDynamic testingEdge cases, boundary values
Foundry InvariantStateful fuzzingState-dependent bugs
EchidnaFuzzerProperty testing (alternative)
MythrilSymbolic executionPath exploration
CertoraFormal verificationMathematical proofs

Conclusion

A production-grade smart contract CI/CD pipeline is fundamentally different from traditional software CI. Every stage serves as a safety gate before irreversible deployment. Gas snapshots catch performance regressions, static analyzers flag vulnerability patterns, fuzzers explore edge cases, and deployment simulations verify behavior against real state. The pipeline’s job is simple: make it harder to deploy a bug than to catch one.