Skip to content
 
 

Latest commit

 

History

486 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

soroban-cost-linter

The static analysis shield for Soroban smart contracts

CI Status License

Documentation · Demo

Part of the Tollcraft initiative.

soroban-cost-linter is a static analysis tool for Stellar smart contract developers. It analyzes your Rust code before compilation to detect input-independent, structurally expensive patterns that would unnecessarily drive up your Soroban resource metering and network fees.

This tool acts as the preventative shield in the Tollcraft two-tiered cost pipeline, pairing conceptually with our runtime test harness, soroban-budget-assert.

The Problem

Soroban charges for CPU instructions, memory allocations, and storage operations. While testing your contract against the network is the only way to measure input-dependent costs (like unbounded loops or dynamic vector sizing), many expensive mistakes are structurally obvious without ever running the code.

Writing env.storage().instance().set() inside a for loop is mathematically guaranteed to be expensive. soroban-cost-linter catches these structural anti-patterns directly in your editor or CI/CD pipeline before they make it to testnet.

Features

The linter hooks into the Rust compiler's AST to catch specific Soroban anti-patterns. Thirty lints ship in v0.1.1:

  • soroban_storage_in_loop: Flags storage read/write operations placed inside loop bodies, suggesting memory aggregation instead.
  • redundant_env_clone: Detects unnecessary .clone() calls on the Soroban Env object.
  • unnecessary_host_function_call: Identifies host accessor calls (Ledger, Crypto, Prng, Events, Deployer, Env::current_contract_address) repeated inside a loop with unchanged inputs, which should be called once and bound to a local variable.
  • storage_write_without_read: Flags storage writes where the same key is never subsequently read.
  • inefficient_bytes_concat: Detects repeated Bytes concatenation inside loops using +, which creates unnecessary per-iteration allocations.
  • map_insert_in_loop: Flags Map::insert calls inside loop bodies.
  • symbol_new_for_short_literal: Flags Symbol::new calls with short literal arguments that could use symbol_short!().
  • bytes_append_in_loop: Flags repeatedly growing SDK containers (Bytes::append, Vec::push_back, Map::insert) inside loops, suggesting native accumulation first.
  • string_concat_in_loop: Flags String::append/String + String on a soroban_sdk::String inside loops, since each concatenation reallocates and copies the whole accumulated string (O(n²)).
  • signature_verification_in_loop: Flags env.crypto().ed25519_verify/secp256k1_recover/secp256r1_verify calls made inside loop bodies, suggesting batch/aggregate verification instead.
  • crypto_hash_of_constant: Flags env.crypto().sha256/keccak256 calls whose input is a literal or const item, since re-hashing a compile-time constant at runtime is pure wasted host cost — precompute and embed the digest instead.
  • vec_where_slice_could_be_used: Flags soroban_sdk::Vec passed by value where a native Rust &[T] slice would be sufficient for read-only access.
  • extend_ttl_in_loop: Flags extend_ttl calls on instance/persistent/temporary storage made inside loop bodies, suggesting batching the TTL extension instead of refreshing per-entry per-iteration.
  • instance_storage_for_unbounded_data: Flags env.storage().instance().set(...) calls where the value is an unbounded Vec/Map/Bytes, since instance storage is re-read and rewritten in full on every contract invocation.
  • formatted_panic_payload: Flags format!, a formatted panic!, or .expect(&format!(..)), all of which pull core::fmt into the contract in place of a cheap panic_with_error! + #[contracterror].
  • val_conversion_chain: Flags a chain of three or more soroban_sdk conversions (into_val/try_into_val/from_val/try_from_val) that bounce the same local value through Val across a let sequence, where converting directly to the needed shape would cost a single host call.

How it Fits into Tollcraft

soroban-cost-linter is designed to be Stage 1 of your cost-awareness pipeline:

  1. Linter (soroban-cost-linter): Runs at compile-time (or via cargo check). Catches obvious, static structural flaws.
  2. Assert (soroban-budget-assert): Runs at test-time. Simulates your cleanly-linted code against the network to measure actual execution costs based on real runtime inputs.

Both tools share configuration via a unified budget.toml file for thresholds and suppressions.

Getting Started

Recommended: Dev Container

The fastest way to get a working environment is the pre-built container image, which ships with the exact nightly toolchain, compiler components, and Dylint binaries installed — no manual setup required.

docker pull ghcr.io/Tollcraft/soroban-cost-linter:latest
docker run --rm -it -v "$(pwd)":/workspace ghcr.io/Tollcraft/soroban-cost-linter:latest bash
# Inside the container:
cargo test --workspace

VS Code / GitHub Codespaces users can open the repo and choose "Reopen in Container" — the .devcontainer/devcontainer.json handles everything automatically.

See CONTRIBUTING.md for full setup details, including a manual local setup path.

Prerequisites

Windows users: the project CI runs on Ubuntu. For the smoothest setup, prefer WSL2 with Ubuntu — see docs/windows_setup.md. Native-PowerShell install is covered in the same page; Visual Studio Build Tools is required because the MSVC rustc toolchain needs link.exe (which Build Tools provides).

Since soroban-cost-linter hooks directly into Rust's AST, it relies on Dylint to run dynamic library lints. The linter library requires Dylint version ^6.0.1.

  1. Install the pinned nightly toolchain — see the rust-toolchain file for the exact channel (as of this writing, the CI uses nightly-2026-04-16).

    rustup toolchain install <channel-from-rust-toolchain>
  2. Install Dylint — the linter relies on Dylint version ^6.0.1 to run dynamic library lints:

    cargo install cargo-dylint dylint-link --version "^6.0.1"

Windows: Install via PowerShell after setting up Rust through rustup. The command is identical. Make sure the nightly toolchain with rustc-dev and llvm-tools-preview components is installed (rustup toolchain install nightly --component rustc-dev llvm-tools-preview).

Installation

Add the linter to your Soroban workspace. Ensure you are using the pinned nightly toolchain (see Prerequisites) when building:

cargo +<channel-from-rust-toolchain> install --git https://github.com/Tollcraft/soroban-cost-linter.git cargo-cost-lint

Why is the nightly required? The lint library links against rustc_private, which is only available on nightly compilers. A different nightly version may produce linker errors due to ABI mismatches.

Verifying a downloaded binary

Each release includes a SHA256SUMS file. After downloading both the binary and the checksums file, run:

sha256sum -c SHA256SUMS

If the binary was not tampered with the output will say cargo-cost-lint: OK.

Quick Start

  1. Complete the Prerequisites (nightly toolchain + Dylint).

  2. Install the linter using the pinned nightly:

    cargo +<channel-from-rust-toolchain> install --git https://github.com/Tollcraft/soroban-cost-linter.git cargo-cost-lint
  3. Run it on your Soroban project:

    cargo cost-lint

Usage

CLI Flags

Flag Description
--config <PATH> Path to budget.toml for lint-level overrides
--allow <LINT>, -A <LINT> Set a lint to allow for this run (repeatable, overrides budget.toml)
--warn <LINT>, -W <LINT> Set a lint to warn for this run (repeatable, overrides budget.toml)
--deny <LINT>, -D <LINT> Set a lint to deny for this run (repeatable, overrides budget.toml)
--package <SPEC>, -p <SPEC> Package(s) to lint (repeatable, restricts linting to specified packages)
--workspace Lint all packages in the workspace
--no-cache Bypass the lint result cache for this run
--clear-cache Clear all cached lint results and exit
--format <text|json|sarif|github> Output format (default: text)
--list-lints Print every registered lint with its default level and one-line description, then exit
--explain <LINT> Print the full documentation for a specific lint (what it does, why it's expensive, suggested fix) and exit
--quiet Suppress informational and warning output (lint findings and errors are never suppressed)
--verbose Show diagnostic detail: resolved config path, lint flags, and the spawned command
--version Print the crate version and exit

--quiet and --verbose are mutually exclusive. In JSON mode (--format json), both flags keep stdout as clean NDJSON; all diagnostic output goes to stderr.

Result Caching

cargo cost-lint automatically caches lint results between runs to make repeat runs on unchanged code near-instant.

Cache Invalidation

The cache key is computed deterministically from:

  • Source Content: Hash of all source code files, Cargo.toml, and Cargo.lock files.
  • Resolved Lint Levels: Effective -A/-W/-D lint flags.
  • Linter Version: The version of cargo-cost-lint.
  • Toolchain: Active rustc compiler version and commit.
  • Package Selection & Output Format: Requested --package/--workspace args and --format.

Modifying any of these inputs automatically invalidates the cache entry and triggers a fresh lint pass.

Bypassing and Clearing the Cache

  • Run with --no-cache to force a fresh run without using cached results.
  • Run with --clear-cache to delete all cached entries.
  • The cache files are stored in target/cost-lint-cache/, which is ignored by Git and cleaned automatically with cargo clean.

Package Selection

In multi-crate workspaces, you can restrict linting to specific packages or explicitly lint the entire workspace:

# Lint a single package
cargo cost-lint -p my-contract

# Lint multiple packages
cargo cost-lint -p contract-a --package contract-b

# Explicitly lint all packages in the workspace
cargo cost-lint --workspace

Default Behavior

When neither --package nor --workspace is specified, cargo cost-lint follows standard Cargo semantics: it lints the package in the current working directory, or all default workspace members if invoked from the root of a virtual workspace.

Passing an unknown package name with --package will be rejected with an error listing the available workspace members.

Command-Line Level Overrides

You can temporarily override lint levels without editing budget.toml using --allow (-A), --warn (-W), and --deny (-D):

# Deny storage operations in loops and allow redundant env clones for this run
cargo cost-lint --deny soroban_storage_in_loop --allow redundant_env_clone

Precedence

  1. Command-line flags (--allow, --warn, --deny) take the highest precedence.
  2. budget.toml defines project-wide defaults for unoverridden lints.
  3. Built-in lint defaults apply when neither the command line nor budget.toml specifies a level.

Configuration Discovery Order

When resolving budget.toml, cargo cost-lint uses the following order:

  1. Explicit --config <PATH> CLI option: Loads the specified configuration file. Fails with an error if the path does not exist.
  2. Current working directory: Checks for budget.toml in the current working directory.
  3. Walk-up discovery: If not found in the current directory, walks up parent directories until it finds the workspace root.
  4. Safe defaults: If no budget.toml is found up to the workspace root, safe default lint levels are used.

You can inspect the resolved configuration path by running with --verbose.

Passing conflicting levels for the same lint (e.g. --allow <LINT> --deny <LINT>) or an unknown lint name will be rejected with an error before execution.

Running the linter

From the root of your Soroban contract workspace:

cargo cost-lint

To inspect the machine-readable lint inventory that the CLI emits, run:

cargo cost-lint --list-lints --format json

The output is a versioned JSON object with the lint name, default level, description, category, and documentation URL for every registered lint.

The linter will analyze all Rust source files and report any Soroban anti-patterns it finds. The output looks like this:

error: storage operation inside a loop
  --> src/lib.rs:12:9
   |
LL |         env.storage().instance().set(&i, &1);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: move storage operations out of the loop or accumulate mutations in memory first
   = note: `#[warn(soroban_storage_in_loop)]` on by default

Output format

Use --format to choose the output format:

Format Description
text Human-readable console output (default)
json One JSON object per line, suitable for programmatic parsing
sarif SARIF v2.1.0 output, compatible with GitHub Code Scanning
github GitHub Actions workflow command annotations

Example — generate SARIF output for GitHub Advanced Security:

cargo cost-lint --format sarif > results.sarif

The SARIF file can then be uploaded to GitHub or integrated into your CI pipeline to annotate PR diffs with line-specific warnings.

warning: storage operation inside a loop
  --> src/lib.rs:12:9
   |
LL |         env.storage().instance().set(&i, &1);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: move storage operations out of the loop or accumulate mutations in memory first
   = note: `#[warn(soroban_storage_in_loop)]` on by default

warning: unnecessary host function call inside loop
  --> src/lib.rs:20:20
   |
LL |         let _seq = env.ledger().sequence();
   |                    ^^^^^^^^^^^^^^^^^^^^^^^
   = help: call this function outside the loop and reuse the result
   = note: `#[warn(unnecessary_host_function_call)]` on by default

warning: redundant clone on Env object
  --> src/lib.rs:30:19
   |
LL |     let _cloned = env.clone();
   |                   ^^^^^^^^^^^
   |
   = help: pass `Env` by reference or value instead of cloning
   = note: `#[warn(redundant_env_clone)]` on by default

lint summary:
  redundant_env_clone: 1
  soroban_storage_in_loop: 3
  unnecessary_host_function_call: 2
total: 6

For detailed examples of each lint and instructions on suppressing false positives, see the docs/lints/ directory.

For instructions on CI/CD integration, configuring the budget.toml file, and setting a maximum warnings threshold, see the Integration Guide.

Contributing

We are actively looking for contributors in cost-model research, AST parsing, and lint specification.

  1. Check the open issues to find tasks labeled good first issue or help wanted.
  2. Fork the repository.
  3. Ensure all Pull Requests target the main branch.
  4. Pass all local tests before submitting.

See CONTRIBUTING.md for more detailed guidelines. Windows contributors, start with docs/windows_setup.md for WSL2 and native-PowerShell setup instructions.

Performance Benchmarking & Regression Gate

The performance benchmark suite measures linter execution duration across all corpus contracts:

# Run the benchmark and compare against the recorded baseline
cargo bench --bench linter_performance --package cargo-cost-lint

# Deliberately update/bless the baseline when a performance slowdown is accepted
BLESS_BENCH=1 cargo bench --bench linter_performance --package cargo-cost-lint

The CI pipeline runs this gate automatically. Regressions exceeding the 25% threshold fail the build.

Release history is documented in CHANGELOG.md.

Community

Join the discussion on our Discord.

Maintainers

Name Role Contact
mallison031 Maintainer GitHub
Tollcraft Team Core Maintainers Tollcraft on Telegram

Contributors

Contributors

Fixing issue 463

Fixing issue 463

Development Container Image

A pre-built development container image containing the pinned nightly Rust toolchain, cargo-dylint, and dylint-link is published automatically for this project. You can pull it from the GitHub Container Registry:

docker pull ghcr.io/tollcraft/soroban-cost-linter-dev:latest

This image does NOT contain the linter itself; it is an environment for building and testing the linter. Mount your source code into /workspace to use it.

About

A static analysis linter for Stellar Soroban smart contracts to catch input-independent resource cost anti-patterns before deployment.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages