Sanctifier’s static analysis engine parses your contract’s Rust source code with theDocumentation 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.
syn crate, constructs a full AST, and runs a suite of detector visitors across every impl block, fn body, and #[contracttype] declaration it finds. Analysis is deterministic and entirely offline — no network calls, no running WASM. Each detector emits structured findings that are aggregated into a JSON report at the end of a run.
How the AST Walk Works
When you runsanctifier analyze ./contracts/my-token, the Analyzer struct in sanctifier-core does the following for each .rs file:
Parse source to AST
syn::parse_str::<syn::File>(source) is called. On parse failure the file is skipped silently — broken syntax produces no false positives.Dispatch to visitors
Each detector is a
syn::visit::Visit implementation that traverses the AST tree. Separate visitors handle auth gaps, panic patterns, arithmetic operators, storage method calls, and event publish calls.Aggregate findings
Individual detector results are collected into the unified
RuleViolation type, each carrying a code, severity, message, and location string. The RuleRegistry folds these into the final report.with_panic_guard (a std::panic::catch_unwind wrapper) so a single malformed expression never crashes the whole scan.
Detector Families
S001 — Authentication Gaps
The auth-gap detector walks everypub fn in every impl block and checks whether a state-mutating function calls require_auth or require_auth_for_args before writing to storage.
Vulnerable pattern:
.set(), .update(), and .remove() method calls for any token that contains "storage", "persistent", "temporary", or "instance".
S002 — Panic Handling
The panic detector visits everysyn::Expr::Macro node looking for panic!, and every syn::ExprMethodCall looking for .unwrap() and .expect(). It fires inside all impl functions, both public and private.
Vulnerable patterns:
SANCT_UNWRAP is emitted specifically for .unwrap(), .expect(), and risky .unwrap_or_default() calls inside #[contractimpl] entrypoints, where an attacker-triggered missing value can abort the transaction or silently initialize financial state to zero.
Preferred alternative:
S003 — Arithmetic Overflow
TheArithVisitor walks syn::ExprBinary and syn::ExprAssign nodes looking for +, -, *, +=, -=, and *= operators in contract impl functions. One finding is emitted per (function_name, operator) pair to keep output actionable — multiple uses of the same operator in one function produce a single diagnostic.
Vulnerable patterns:
suggestion field names the corresponding checked_* method (checked_add, checked_sub, checked_mul).
S004 — Storage Limits
The ledger-size detector walks#[contracttype] structs and enums, estimates the serialized byte size of each field using conservative type-size heuristics, and emits a warning when the total approaches or exceeds the configured ledger_limit (default 64,000 bytes, matching Stellar’s 64 KB ledger-entry limit).
| Type | Estimated bytes |
|---|---|
u32, i32, bool | 4 |
u64, i64 | 8 |
u128, i128, I128, U128 | 16 |
Address | 32 |
Bytes, BytesN, String, Symbol | 64 |
Vec<T> | 8 + size(T) |
Map<K, V> | 16 + 2 × (size(K) + size(V)) |
approaching_threshold config (default 0.8) controls when the ApproachingLimit level fires versus ExceedsLimit.
S005 — Storage Collisions
TheStorageVisitor tracks every storage key — string literals, Symbol::new calls, symbol_short! macros, and named constants — grouped by storage type (Instance, Persistent, Temporary). After the full file walk, final_check() emits a StorageCollisionIssue for each key that appears in the same storage tier more than once.
Vulnerable pattern:
instance, persistent, temporary) is not flagged — Stellar’s storage model isolates those namespaces at the host level.
S006 — Unsafe Patterns
TheUnsafeVisitor is a file-scope visitor (broader than S002) that walks every node in the entire parsed AST for panic! macros, .unwrap(), and .expect() method calls. Unlike S002, which is limited to impl blocks, S006 fires anywhere in the file including helper functions, trait impls, and module-level items.
Example triggers:
proc-macro2 span locations. S006 and S002 can overlap when the same call site is inside an impl block — both codes will be emitted independently.
Fix: Apply the same remediation as S002 — replace unwrap()/expect() with ? or explicit match/if let, and replace panic! with panic_with_error! for typed contract errors.
S007 — Custom Rules
Custom rules let you define project-specific lint patterns as regular expressions in your.sanctify.toml configuration.
regex::Regex. A CustomRuleMatch is emitted for every line where the pattern fires, carrying the rule name, line number, source snippet, and severity.
S008 — Event Inconsistency
The event scanner checks calls toenv.events().publish(topics, data) across the entire file and flags two patterns:
- Inconsistent topic counts: the same event name is emitted with different numbers of topics in different code paths.
- Gas-optimizable topics: string topics that could be replaced with
symbol_short!to save host gas.
JSON Report Structure
Runningsanctifier analyze --json produces a report with the following top-level keys:
findings always carries a code field that corresponds to a row in error_codes. See the Finding Codes Reference for a complete table.
Configuring the Scan
Create a.sanctify.toml at the workspace root to control which detectors run and how thresholds are applied:
The
enabled_rules list is declarative today: any name present in the list
activates the corresponding built-in detector. Rules not listed are still
loaded by the RuleRegistry but their findings are omitted from the final
report. This means adding new rules to the registry never changes existing CI
baselines unless the name also appears in your config.