Sanctifier provides a proc-macro attribute,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.
#[sanctify::invariant(expr)], that lets you declare contract invariants directly in source code alongside the #[contractimpl] block they describe. In a normal build the attribute is completely transparent — the production WASM binary is identical to the unannotated version. Under cargo kani the attribute additionally emits a #[kani::proof] harness, and the sanctifier verify command reports proof results through its Z3 SMT fast-path or by delegating to Kani.
The contracts/token-invariants crate is the canonical example of this pattern and ships with six formal proof harnesses.
Adding the proc-macro crate
Thesanctify-macros crate lives in tooling/sanctify-macros. Add it to your contract’s Cargo.toml:
use import at the top of your contract file:
Declaring an invariant
Place#[invariant(expr)] immediately above #[contractimpl]. The expression is any valid Rust expression — typically a call to a pure helper function that returns bool. In contracts/token-invariants/src/lib.rs:
#[invariant(...)] is the pure function that encodes the property. Here, pure::supply_is_conserved_after_transfer(0, 0, 0) is the function defined in contracts/token-invariants/src/pure.rs:
What happens in each build mode
Normal build
The
#[invariant(...)] attribute is a pass-through. The impl block is emitted unchanged. The expression is parsed and stored internally for diagnostic use but no runtime assertion is injected. The production WASM binary is bit-for-bit identical to the unannotated version.Kani build (`--cfg kani`)
Under
cargo kani, the macro additionally emits a #[kani::proof] harness that calls the invariant expression with symbolic inputs. Kani then formally proves — or refutes — the property for all possible values.No runtime assertion is ever injected into the production binary. Soroban contracts panic on assertion failure and invariant expressions may reference functions only meaningful in a testing context. A future
--runtime-checks flag will opt in to inline assertions if you want them.Z3 SMT fast-path
When you runsanctifier verify, it first attempts a Z3 SMT fast-path before invoking Kani. The fast-path can prove:
- Integer equalities — expressions of the form
a == bwhereaandbare linear combinations of integer literals. - Tautologies — expressions that are trivially true regardless of input, such as
trueorx == x.
supply_is_conserved_after_transfer — the verifier routes to Kani.
This is the same proof-strategy split used in the contracts/kani-poc crate, where pure arithmetic harnesses are amenable to SMT reasoning and the contract layer is left unverified.
Using sanctifier verify
Output states
| Status | Meaning |
|---|---|
PROVEN | The Z3 SMT solver proved the invariant holds for all inputs |
REFUTED | A counterexample was found — the invariant is violated |
UNKNOWN | The solver hit its time budget without a definitive answer |
KANI ↗ | The invariant requires Kani for a full proof; run cargo kani |
sanctifier verify run on contracts/token-invariants produces output similar to:
Proof harnesses in contracts/token-invariants
The kani_proofs.rs module in contracts/token-invariants ships six harnesses that formally verify the pure arithmetic layer:
Why the production binary is unchanged
The design deliberately avoids injecting any runtime assertion into the production WASM. There are two reasons:- Safety: Soroban contracts panic on assertion failure, and a panic in production causes the entire transaction to revert. Injecting assertions on invariant expressions — which may call pure testing helpers — would introduce unexpected failure modes in deployed contracts.
- Correctness boundary: The invariant expression is a property about the logic, not the state. It is verified once, statically, by the SMT solver or Kani. Re-evaluating it at runtime every time the function is called would be both expensive and semantically different from what the attribute declares.
SanctifiedGuard trait in sanctifier-core provides an explicit before/after guard pattern. See the Runtime Guards guide for details.