Part of the
Tollcraftinitiative.
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.
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.
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 SorobanEnvobject.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 repeatedBytesconcatenation inside loops using+, which creates unnecessary per-iteration allocations.map_insert_in_loop: FlagsMap::insertcalls inside loop bodies.symbol_new_for_short_literal: FlagsSymbol::newcalls with short literal arguments that could usesymbol_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: FlagsString::append/String + Stringon asoroban_sdk::Stringinside loops, since each concatenation reallocates and copies the whole accumulated string (O(n²)).signature_verification_in_loop: Flagsenv.crypto().ed25519_verify/secp256k1_recover/secp256r1_verifycalls made inside loop bodies, suggesting batch/aggregate verification instead.crypto_hash_of_constant: Flagsenv.crypto().sha256/keccak256calls whose input is a literal orconstitem, 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: Flagssoroban_sdk::Vecpassed by value where a native Rust&[T]slice would be sufficient for read-only access.extend_ttl_in_loop: Flagsextend_ttlcalls 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: Flagsenv.storage().instance().set(...)calls where the value is an unboundedVec/Map/Bytes, since instance storage is re-read and rewritten in full on every contract invocation.formatted_panic_payload: Flagsformat!, a formattedpanic!, or.expect(&format!(..)), all of which pullcore::fmtinto the contract in place of a cheappanic_with_error!+#[contracterror].val_conversion_chain: Flags a chain of three or moresoroban_sdkconversions (into_val/try_into_val/from_val/try_from_val) that bounce the same local value throughValacross aletsequence, where converting directly to the needed shape would cost a single host call.
soroban-cost-linter is designed to be Stage 1 of your cost-awareness pipeline:
- Linter (
soroban-cost-linter): Runs at compile-time (or viacargo check). Catches obvious, static structural flaws. - 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.
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 --workspaceVS 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.
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
rustctoolchain needslink.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.
-
Install the pinned nightly toolchain — see the
rust-toolchainfile for the exact channel (as of this writing, the CI usesnightly-2026-04-16).rustup toolchain install <channel-from-rust-toolchain>
-
Install Dylint — the linter relies on Dylint version
^6.0.1to 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-devandllvm-tools-previewcomponents is installed (rustup toolchain install nightly --component rustc-dev llvm-tools-preview).
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-lintWhy 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.
Each release includes a SHA256SUMS file. After downloading both the binary and the checksums file, run:
sha256sum -c SHA256SUMSIf the binary was not tampered with the output will say cargo-cost-lint: OK.
-
Complete the Prerequisites (nightly toolchain + Dylint).
-
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
-
Run it on your Soroban project:
cargo cost-lint
| 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.
cargo cost-lint automatically caches lint results between runs to make repeat runs on unchanged code near-instant.
The cache key is computed deterministically from:
- Source Content: Hash of all source code files,
Cargo.toml, andCargo.lockfiles. - Resolved Lint Levels: Effective
-A/-W/-Dlint flags. - Linter Version: The version of
cargo-cost-lint. - Toolchain: Active
rustccompiler version and commit. - Package Selection & Output Format: Requested
--package/--workspaceargs and--format.
Modifying any of these inputs automatically invalidates the cache entry and triggers a fresh lint pass.
- Run with
--no-cacheto force a fresh run without using cached results. - Run with
--clear-cacheto delete all cached entries. - The cache files are stored in
target/cost-lint-cache/, which is ignored by Git and cleaned automatically withcargo clean.
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 --workspaceWhen 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.
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- Command-line flags (
--allow,--warn,--deny) take the highest precedence. budget.tomldefines project-wide defaults for unoverridden lints.- Built-in lint defaults apply when neither the command line nor
budget.tomlspecifies a level.
When resolving budget.toml, cargo cost-lint uses the following order:
- Explicit
--config <PATH>CLI option: Loads the specified configuration file. Fails with an error if the path does not exist. - Current working directory: Checks for
budget.tomlin the current working directory. - Walk-up discovery: If not found in the current directory, walks up parent directories until it finds the workspace root.
- Safe defaults: If no
budget.tomlis 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.
From the root of your Soroban contract workspace:
cargo cost-lintTo inspect the machine-readable lint inventory that the CLI emits, run:
cargo cost-lint --list-lints --format jsonThe 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
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.sarifThe 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.
We are actively looking for contributors in cost-model research, AST parsing, and lint specification.
- Check the open issues to find tasks labeled
good first issueorhelp wanted. - Fork the repository.
- Ensure all Pull Requests target the
mainbranch. - 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.
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-lintThe CI pipeline runs this gate automatically. Regressions exceeding the 25% threshold fail the build.
Release history is documented in CHANGELOG.md.
Join the discussion on our Discord.
| Name | Role | Contact |
|---|---|---|
| mallison031 | Maintainer | GitHub |
| Tollcraft Team | Core Maintainers | Tollcraft on Telegram |
Fixing issue 463
Fixing issue 463
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:latestThis 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.