Formal verification goes further than static analysis or runtime guards: it proves that a property holds for every possible input, not just the ones your tests happen to exercise. Sanctifier provides two complementary formal verification paths — an SMT-based prover backed by Z3 for algebraic tautologies, and Kani harnesses for exhaustive symbolic execution of pure Rust functions. Understanding when to use each approach, and why Soroban’s host types make the pure-function separation pattern essential, is the foundation of effective contract verification.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Centurylong/sanctifier/llms.txt
Use this file to discover all available pages before exploring further.
Two Verification Approaches
SMT via Z3 (sanctifier prove)
Proves integer equalities and tautologies symbolically. Ideal for supply conservation, balance non-negativity, and arithmetic invariants that can be expressed as closed-form constraints.
Kani Harnesses (cargo kani)
Symbolically executes pure Rust functions. Proves correctness of transfer, mint, burn, and initialization logic for all possible
i128 inputs within specified bounds.SMT-Based Verification with Z3
sanctifier prove dispatches contract invariants to the Z3 SMT solver via SmtProver. The prover encodes token-contract invariants as constraint satisfaction problems and asks Z3 to find a violation: if Z3 returns UNSAT (no violation exists), the invariant is PROVEN; if it returns SAT, it provides a concrete counterexample.
Built-in Token Invariants
TheSmtProver ships with three built-in invariants for token contracts:
| Invariant | Description | Typical Result |
|---|---|---|
balance_non_negative | Checks whether an unchecked transfer can drive a holder’s balance below zero. | VIOLATED — no underflow guard means the model finds a trivial counterexample. |
supply_conserved | Proves that new_from + new_to == balance_from + balance_to holds for all valid (bounds-checked) transfers. | PROVED — supply conservation is a mathematical certainty under valid preconditions. |
no_unauthorized_mint | Checks whether a mint without require_auth allows arbitrary callers to inflate supply. | VIOLATED — any caller can mint when the auth check is missing. |
Running the Prover
How the Supply Conservation Proof Works
Theprove_supply_conserved method models a transfer as pure integer arithmetic, asserts the negation of the conservation property, and checks satisfiability:
SmtInvariantVerifier for Inline Invariants
For invariants declared with #[sanctify::invariant(...)], the SmtInvariantVerifier handles two fast-path cases without requiring Z3 session setup:
- Integer literal equalities (
42 == 42): proven by constant comparison. - Tautological equalities (
x == x): proven by token identity.
Unsupported and is redirected to Kani.
Kani Harnesses for Pure Functions
Kani is a formal verification tool that symbolically executes Rust code. It works by treating function arguments as fully unconstrained symbolic values, then exhaustively checking whether any assertion can fail. For Soroban contracts, however, there is a fundamental obstacle.Why Soroban Host Types Block Kani
Soroban contracts interact with the blockchain viasoroban_sdk::Env. Every Env method eventually calls extern "C" host functions that are defined at the Stellar VM layer — outside of any Rust source Kani can see.
| Type | Limitation for Kani |
|---|---|
Env | Opaque handle; all storage, event, auth, and crypto operations are FFI. |
Val | Runtime value type; conversion and comparison require host calls. |
Symbol | Opaque u32 handle; symbol_short!, comparison, and display need the host. |
Address | Opaque handle; require_auth, comparison, and display require host. |
String, Bytes, Map, Vec | Host-backed types; all operations delegate to env FFI. |
#[contractimpl] block that reads from env.storage() results in unresolvable foreign-function stubs and state-space explosion.
The Core Logic Separation Pattern
The solution is to extract all verifiable business logic into pure Rust functions that take only primitive types and returnResult. The contract layer becomes a thin wrapper that loads data from storage, calls the pure function, and writes results back.
Pure Functions from contracts/kani-poc
These four functions from contracts/kani-poc/src/lib.rs are the canonical examples of the pattern:
Kani Harnesses
Harnesses live in#[cfg(kani)] mod verification blocks so they compile only when Kani is active. Each harness uses kani::any() to generate unconstrained symbolic values and kani::assume() to restrict the input domain to valid states.
Supply Conservation (verify_transfer_pure_conservation)
Full Harness Inventory
| Harness | Property Proved |
|---|---|
verify_transfer_pure_conservation | new_from + new_to == balance_from + balance_to for all valid inputs |
verify_transfer_pure_rejects_non_positive_amount | transfer_pure always returns Err when amount <= 0 |
verify_transfer_pure_rejects_underflow | transfer_pure returns Err on i128 underflow |
verify_mint_pure | Mint produces balance + amount for all valid inputs |
verify_burn_pure | Burn produces balance - amount for all valid inputs |
verify_initialize_fails_when_already_initialized | initialize_pure(true) always returns Err |
verify_initialize_succeeds_when_not_initialized | initialize_pure(false) always returns Ok |
verify_initialize_idempotency_guarantee | Exhaustive over all bool states: double-initialization is mathematically impossible |
The #[sanctify::invariant] Proc-Macro
The sanctify-macros crate provides a proc-macro attribute that attaches an invariant expression to an impl block. In a normal build the attribute is transparent — the impl block is emitted unchanged. Under cargo kani (--cfg kani), the macro additionally emits a #[kani::proof] harness that asserts the invariant expression.
sanctifier verify scans source files for this attribute and dispatches invariant expressions to either the Z3 fast path (integer equalities and tautologies) or the Kani path (everything else).