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.
npm install
npm run buildThe CLI is available as sol-deps after linking, or directly via npx tsx src/index.ts.
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.
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-projectReads 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.lockOutput:
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
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 installOutput:
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.
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-claudeOutput:
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:
- Fetches verified source code and deploy timestamp from Etherscan's v2 API
- Parses sources into project files vs dependency groups (handles
@scope/pkg,lib/name,node_modules/path conventions, and flattened single-file contracts) - Maps each dependency group to a GitHub repository using known mappings for common Solidity deps, GitHub search, or optionally a Claude agent for unknowns
- For each identified repo, computes git object hashes (
blob <size>\0<content>→ SHA-1) for the dependency's files, then searches the repo withgit log --find-objectto find commits containing those exact file contents - 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).
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_xxxCopy .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).
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.
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 |
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.
Content checksums are computed by:
- Collecting all files in the dependency directory (respecting
.gitignore, excludinglib/,node_modules/,.git/) - Sorting files by relative path (forward-slash separated, lexicographic)
- Feeding each file into a SHA-512 hash:
relative_path + NUL + file_contents - 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.
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