This document outlines the limitations and challenges encountered when integrating Kani Rust Verifier with Soroban smart contracts, and describes the proof-of-concept harness for a standard token contract.
Kani is a formal verification tool for Rust that can prove properties about code by symbolically executing it. It works best on pure Rust code with minimal external dependencies or FFI calls.
Soroban contracts interact with the blockchain environment via soroban_sdk::Env. The Env trait methods (storage, events, crypto, auth) eventually call extern "C" functions provided by the host. Kani cannot verify these calls because:
- They are external functions with no implementation visible during contract compilation
- Kani requires source code or a verified model of all called code to reason symbolically
- The host ABI is defined at the Stellar VM layer, not in Rust
| Type | Limitation for Kani |
|---|---|
Env |
Opaque handle; all operations (storage get/set, require_auth, events) are FFI |
Val |
Runtime value type; conversion and comparison require host calls |
Symbol |
Opaque (u32) handle; symbol_short!, comparison, and display need host |
Address |
Opaque handle; require_auth, comparison, and display require host |
String, Bytes, Map, Vec |
Host-backed types; operations delegate to env FFI |
Implications:
- Logic that depends on the content of these types cannot be verified without a full host model
- Treating them as integers (e.g. u64) loses semantic meaning and host-enforced invariants
- Authentication flows (
require_auth) cannot be modelled by Kani
While soroban-env-host provides a Rust implementation of the host environment (used for local testing), verifying contracts linked against it is challenging:
- Complexity: The host environment is large and complex, involving memory management, storage emulation, and VM logic.
- State Explosion: Symbolic execution of the entire host stack leads to state space explosion, making verification extremely slow or infeasible for non-trivial contracts.
- Dependencies: The host environment pulls in many dependencies which increase the verification burden.
To leverage Kani effectively, we recommend a "Core Logic Separation" pattern:
- Isolate Logic: Extract critical business logic into pure Rust functions that do not depend on
soroban_sdk::Envor host types. - Verify Pure Functions: Use Kani to verify these pure functions against properties (e.g., no overflow, invariant preservation).
- Thin Contract Layer: Keep the actual contract implementation (the
#[contractimpl]block) as a thin layer that only marshals data between the host environment and the verified pure logic.
In contracts/kani-poc/src/lib.rs, we extract pure balance and initialisation logic from a standard Soroban token:
// Verified with Kani — pure primitives only
pub fn initialize_pure(is_initialized: bool) -> Result<(), &'static str> { ... }
pub fn transfer_pure(balance_from: i128, balance_to: i128, amount: i128) -> Result<(i128, i128), &'static str> { ... }
pub fn mint_pure(balance: i128, amount: i128) -> Result<i128, &'static str> { ... }
pub fn burn_pure(balance: i128, amount: i128) -> Result<i128, &'static str> { ... }
// Not verified — Host types, FFI
pub fn set_admin(env: Env, new_admin: Symbol) {
env.storage().instance().set(...); // Requires Env, Symbol
}A production contract would keep the #[contractimpl] layer thin: load balances from env.storage(), call the pure functions, then write back.
The Kani harnesses in contracts/kani-poc prove:
| Harness | Property |
|---|---|
verify_initialize_fails_when_already_initialized |
initialize always returns Err when the contract is already set up |
verify_initialize_succeeds_when_not_initialized |
initialize always returns Ok on a fresh contract |
verify_initialize_idempotency_guarantee |
Exhaustive over all boolean states: double-initialisation is mathematically impossible |
verify_transfer_pure_conservation |
Transfer preserves total supply: new_from + new_to == balance_from + balance_to |
verify_transfer_pure_insufficient_balance |
Transfer fails with Err when balance_from < amount |
verify_mint_pure |
Mint correctly adds amount to balance |
verify_burn_pure |
Burn correctly subtracts amount from balance |
Requires Kani to be installed:
cargo install --locked kani-verifier
cargo kani setupThen run:
cargo kani --package kani-poc-contractTo compare proof strategy cost in CI/local testing, Sanctifier now includes an ignored benchmark test in sanctifier-core:
cargo test -p sanctifier-core --test smt_latency_benchmark -- --ignoredThis runs a set of SMT proof strategies repeatedly and writes target/smt-latency-report.json with:
- per-strategy min/avg/max latency (microseconds)
- p95 latency
- a sortable summary for identifying the most expensive strategy
For a step-by-step walkthrough format, including how to read reports and write your first harness, see: