Skip to content

Repository files navigation

sol-deps

Solidity dependency parser, lockfile generator, and dependency verifier. Produces a contracts.lock file that captures every resolved dependency (direct and transitive) with its version, source, and content checksum, enabling reproducible builds and supply-chain auditing for Solidity projects.

Install

npm install
npm run build

The CLI is available as sol-deps after linking, or directly via npx tsx src/index.ts.

GitHub Action

sol-deps ships as a reusable composite action that other Solidity repos can use to automatically submit dependencies to the GitHub Dependency Submission API.

Add the following workflow to your repository (e.g. .github/workflows/sbom.yml):

name: SBOM

on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: awilliams1-cb/sol-deps@<commit-sha>

If a contracts.lock file already exists in your project, the action submits it directly. Otherwise it generates one automatically. The project name and GitHub token are derived automatically from the workflow context.

Commands

generate -- Create a lockfile

Scans a Solidity project, resolves all dependencies, computes content checksums, and writes a contracts.lock file.

sol-deps generate [projectDir] --name <org/name> [options]

Arguments:

Argument Description Default
projectDir Path to the Solidity project .

Options:

Option Alias Description Default
--name -n Project name in org/name format (required) --
--version -v Project version (auto-detects from git tags if omitted) git tag
--output -o Output file path contracts.lock
--github-token -- GitHub API token for resolving orphaned deps (also reads GITHUB_TOKEN env var) --

Example:

# Generate lockfile for a Foundry project
sol-deps generate ./my-project --name myorg/my-project

# Specify version and output path
sol-deps generate ./my-project --name myorg/my-project --version 1.0.0 --output locks/contracts.lock

# With GitHub token for better API rate limits
GITHUB_TOKEN=ghp_xxx sol-deps generate ./my-project --name myorg/my-project

verify -- Check dependency integrity

Reads an existing contracts.lock file and checks that every dependency on disk matches its recorded checksum. Exits with code 1 if any dependencies are missing or have mismatched checksums.

sol-deps verify [projectDir] [options]

Options:

Option Description Default
--lockfile Path to lockfile contracts.lock

Example:

# Verify deps in current directory
sol-deps verify

# Verify a specific project against a specific lockfile
sol-deps verify ./my-project --lockfile ./my-project/contracts.lock

Output:

Verifying 14 dependencies from contracts.lock...
  OK       foundry-rs/forge-std@1.15.0
  OK       OpenZeppelin/openzeppelin-contracts@5.6.1
  MISMATCH Vectorized/solady@0.1.1
           expected: sha512-abc...
           actual:   sha512-def...
  MISSING  a16z/erc4626-tests@0.0.1

Verification complete: 12 matched, 1 mismatched, 1 missing

install -- Reproduce dependencies from a lockfile

Downloads all dependencies listed in a contracts.lock file and places them at their recorded install_path. Skips any dependency already present with a matching checksum. After downloading, verifies that the content checksum matches the lockfile.

sol-deps install [projectDir] [options]

Options:

Option Description Default
--lockfile Path to lockfile contracts.lock
--github-token GitHub API token for rate limits (also reads GITHUB_TOKEN env var) --
--force Re-download all deps even if checksums match false

Example:

# Install deps from lockfile
sol-deps install ./my-project --lockfile ./my-project/contracts.lock

# Force re-download everything
GITHUB_TOKEN=ghp_xxx sol-deps install ./my-project --force

# Install into current directory using default lockfile path
sol-deps install

Output:

Installing 14 dependencies from contracts.lock...
  SKIP     foundry-rs/forge-std@1.15.0 (already installed)
  INSTALL  OpenZeppelin/openzeppelin-contracts@5.6.1 → lib/openzeppelin-contracts
  INSTALL  a16z/erc4626-tests@0.0.1 → lib/openzeppelin-contracts/lib/erc4626-tests
  UPDATE   Vectorized/solady@0.1.1 (checksum mismatch, re-downloading)

Install complete: 3 installed, 1 skipped, 0 failed

Note: install produces a plain file tree (no .git directories or git submodule setup). It is intended for reproducibility and verification, not as a replacement for forge install.

inspect -- Reconstruct lockfile from a deployed contract

Fetches verified source code for a deployed contract from Etherscan, identifies all dependencies, and uses git object hashing to locate the exact version of each dependency used at deploy time. Produces a reconstructed contracts.lock lockfile — enabling supply-chain auditing for any verified contract without access to the original project repository.

sol-deps inspect <address> --chain <chainId> [options]

Options:

Option Alias Description Default
--chain -c Chain ID (required) --
--etherscan-key -e Etherscan API key (also reads ETHERSCAN_API_KEY env var) --
--github-token -- GitHub token for cloning repos (also reads GITHUB_TOKEN env var) --
--output -o Output lockfile path contracts.lock
--name -n Project name override contract name
--use-claude -- Enable Claude agent for identifying unknown repos (requires LLM_GATEWAY_API_KEY) false

Example:

# Inspect a verified contract on Base
ETHERSCAN_API_KEY=xxx GITHUB_TOKEN=ghp_xxx sol-deps inspect \
  0x7458bfdc30034eb860b265e6068121d18fa5aa72 \
  --chain 8453 --name coinbase/cbBTC

# Inspect on Ethereum mainnet with Claude agent for unknown deps
sol-deps inspect 0xdead...beef --chain 1 --use-claude

Output:

Inspecting contract 0x7458...aa72 on chain 8453 (Base)...
  Contract: cbBTC (compiler v0.8.22)
  Deploy time: 2024-08-26T12:39:21Z
  Found 3 dependency groups in verified source

  Identifying repositories...
    @openzeppelin/contracts              → OpenZeppelin/openzeppelin-contracts (known)
    lib/solady                           → Vectorized/solady (known)
    lib/custom-lib                       → unresolved (use --use-claude to identify)

  Resolving versions via git object hashes...
    OpenZeppelin/openzeppelin-contracts: cloning...
      SafeERC20.sol             → 93 boundary commits, 42 before deploy
      ERC20.sol                 → 87 boundary commits, 40 before deploy
      Intersection              → 38 viable commits
      Best match                → 5.0.2 (tag: v5.0.2)
    Vectorized/solady: cloning...
      SafeTransferLib.sol       → 15 boundary commits, 12 before deploy
      Best match                → 0.0.235 (tag: 0.0.235)

  Writing contracts.lock...

Inspect complete: 2 resolved, 1 unresolved

How it works:

  1. Fetches verified source code and deploy timestamp from Etherscan's v2 API
  2. Parses sources into project files vs dependency groups (handles @scope/pkg, lib/name, node_modules/ path conventions, and flattened single-file contracts)
  3. Maps each dependency group to a GitHub repository using known mappings for common Solidity deps, GitHub search, or optionally a Claude agent for unknowns
  4. For each identified repo, computes git object hashes (blob <size>\0<content> → SHA-1) for the dependency's files, then searches the repo with git log --find-object to find commits containing those exact file contents
  5. Intersects viable commits across all files in a dependency, filters to commits before the deploy timestamp, and resolves to the best matching version tag (or a pseudo-version if untagged)

Supported chains: Any chain supported by Etherscan's v2 API (Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, and more).

submit -- Submit to GitHub Dependency Graph

Resolves dependencies and submits them to the GitHub Dependency Submission API as an SBOM snapshot. This makes Solidity dependencies visible in GitHub's dependency graph and Dependabot alerts.

sol-deps submit [projectDir] --name <org/name> --repo <owner/repo> [options]

Options:

Option Alias Description Default
--name -n Project name in org/name format (required) --
--repo -r GitHub repo in owner/repo format (required) --
--version -v Project version git tag
--output -o Also write a contracts.lock file --
--github-token -- GitHub API token (required; also reads GITHUB_TOKEN env var) --
--sha -- Override HEAD commit SHA auto-detected
--ref -- Override branch ref auto-detected
--job-id -- CI job ID for correlator timestamp-based

Example:

# Submit from CI
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} sol-deps submit \
  --name myorg/my-contracts \
  --repo myorg/my-contracts

# Submit and also generate a lockfile
sol-deps submit ./my-project \
  --name myorg/my-project \
  --repo myorg/my-project \
  --output contracts.lock \
  --github-token ghp_xxx

Environment Variables

Copy .env.example to .env.local and fill in the values:

cp .env.example .env.local
Variable Required for Description
ETHERSCAN_API_KEY inspect Etherscan API key for fetching verified source code (get one here)
GITHUB_TOKEN inspect, submit, generate GitHub personal access token for API rate limits, cloning repos, and dependency submission
LLM_GATEWAY_URL inspect --use-claude Base URL for the LLM gateway
LLM_GATEWAY_API_KEY inspect --use-claude API key for the LLM gateway

All variables can also be passed as CLI flags (e.g. --etherscan-key, --github-token).

Dependency Sources

sol-deps detects dependencies from four sources:

Source How it's detected Example
Foundry foundry.lock + .gitmodules + git submodules lib/forge-std
Soldeer soldeer.toml or [dependencies] in foundry.toml dependencies/forge-std-1.8.1
Orphaned Directories in lib/ not tracked by git or Foundry Broken submodules, copied deps
NPM package.json dependencies containing .sol files node_modules/@openzeppelin/contracts

Transitive dependencies are resolved recursively by scanning each dependency's own lib/ directory.

Lockfile Format

The contracts.lock file uses TOML format:

name = "myorg/my-project"
version = "1.0.0"

[[dependency]]
name = "foundry-rs/forge-std"
version = "1.15.0"
source = "git+https://github.com/foundry-rs/forge-std"
checksum = "sha512-abc123..."
type = "direct"
install_path = "lib/forge-std"
commit_hash = "abc123def456789012345678901234567890abcd"
nested_dependencies = [
  "foundry-rs/forge-std 1.14.0",
]

[[dependency]]
name = "OpenZeppelin/openzeppelin-contracts"
version = "5.6.1"
source = "git+https://github.com/OpenZeppelin/openzeppelin-contracts"
checksum = "sha512-def456..."
type = "transitive"
install_path = "lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts"
commit_hash = "def456789012345678901234567890abcdef0123"

Fields per entry:

Field Description
name Dependency name in org/name format
version Resolved version (semver tag or pseudo-version)
source Source URL (git+https://github.com/...)
checksum SHA-512 content hash in SRI format
type direct (top-level) or transitive (nested)
install_path Relative path from project root where the dep is installed
commit_hash Git commit SHA (omitted for npm/registry deps)
nested_dependencies List of immediate sub-dependencies as "name version" strings

Versioning

Versions are resolved using git tags. When a dependency is not at an exact tag, a pseudo-version is generated:

<base-tag>-<unix-timestamp>-<full-commit-hash>

For example: 0.0.235-1724696571-d87a6baaea980b54f6d0f2d3a3c30c45a5b1520a

If no tags exist at all, the base version is 0.0.0.

Checksums

Content checksums are computed by:

  1. Collecting all files in the dependency directory (respecting .gitignore, excluding lib/, node_modules/, .git/)
  2. Sorting files by relative path (forward-slash separated, lexicographic)
  3. Feeding each file into a SHA-512 hash: relative_path + NUL + file_contents
  4. Formatting the digest as an SRI integrity string: sha512-<base64>

This produces a deterministic, platform-independent content hash that anyone can reproduce from the same files.

Development

npm install
npm run build    # Compile TypeScript
npm test         # Run tests

# Run directly without building
npx tsx src/index.ts generate ./example-project --name example/project

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages