Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions contracts/vulnerable-contract/.sanctifier_cache
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"files": {
"contracts/vulnerable-contract/src/lib.rs": {
"hash": "fa33a5ae635b683cfd78ac007054918fa48009e9e3b014ab5236ecfca10d844d",
"results": {
"size_warnings": [],
"unsafe_patterns": [
{
"pattern_type": "Expect",
"line": 0,
"snippet": "contracts/vulnerable-contract/src/lib.rs:env . storage () . instance () . get (& symbol_short ! (\"admin\")) . expect (\"Admin not set\")"
},
{
"pattern_type": "Panic",
"line": 31,
"snippet": "contracts/vulnerable-contract/src/lib.rs:panic ! (\"Something went wrong\")"
}
],
"auth_gaps": [
"contracts/vulnerable-contract/src/lib.rs:set_admin"
],
"panic_issues": [
{
"function_name": "set_admin_secure",
"issue_type": "expect",
"location": "contracts/vulnerable-contract/src/lib.rs:set_admin_secure"
},
{
"function_name": "fail_explicitly",
"issue_type": "panic!",
"location": "contracts/vulnerable-contract/src/lib.rs:fail_explicitly"
}
],
"arithmetic_issues": [],
"deprecated_issues": [],
"custom_matches": []
}
}
}
}
24 changes: 23 additions & 1 deletion tooling/sanctifier-cli/src/commands/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use clap::Args;
use colored::*;
use sanctifier_core::{Analyzer, ArithmeticIssue, SizeWarning, UnsafePattern};
use sanctifier_core::{Analyzer, ArithmeticIssue, RuleViolation, SizeWarning, UnsafePattern};

#[derive(Args, Debug)]
pub struct AnalyzeArgs {
Expand Down Expand Up @@ -49,6 +49,7 @@ pub fn exec(args: AnalyzeArgs) -> anyhow::Result<()> {
let mut all_auth_gaps: Vec<String> = Vec::new();
let mut all_panic_issues = Vec::new();
let mut all_arithmetic_issues: Vec<ArithmeticIssue> = Vec::new();
let mut all_deprecated_issues: Vec<RuleViolation> = Vec::new();

if path.is_dir() {
analyze_directory(
Expand All @@ -59,6 +60,7 @@ pub fn exec(args: AnalyzeArgs) -> anyhow::Result<()> {
&mut all_auth_gaps,
&mut all_panic_issues,
&mut all_arithmetic_issues,
&mut all_deprecated_issues,
);
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(content) = fs::read_to_string(path) {
Expand Down Expand Up @@ -155,6 +157,19 @@ pub fn exec(args: AnalyzeArgs) -> anyhow::Result<()> {
println!("\nNo arithmetic overflow risks found.");
}

if !all_deprecated_issues.is_empty() {
println!("\n{} Found deprecated Soroban host functions!", "⚠️".yellow());
for issue in all_deprecated_issues {
println!(
" -> Function: {}\n Suggestion: {}",
issue.location.yellow(),
issue.message.cyan()
);
}
} else {
println!("\n{} No deprecated Soroban host functions found.", "✅".green());
}

println!("\nNo upgrade pattern issues found.");
}

Expand Down Expand Up @@ -182,6 +197,7 @@ fn analyze_directory(
all_auth_gaps: &mut Vec<String>,
all_panic_issues: &mut Vec<sanctifier_core::PanicIssue>,
all_arithmetic_issues: &mut Vec<ArithmeticIssue>,
all_deprecated_issues: &mut Vec<RuleViolation>,
) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
Expand Down Expand Up @@ -218,6 +234,12 @@ fn analyze_directory(
a.location = format!("{}: {}", path.display(), a.location);
all_arithmetic_issues.push(a);
}

let deprecated = analyzer.scan_deprecated_host_fns(&content);
for mut d in deprecated {
d.location = format!("{}:{}", path.display(), d.location);
all_deprecated_issues.push(d);
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions tooling/sanctifier-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod gas_estimator;
pub mod kani_bridge;
pub mod zk_proof;
pub mod rules;

use serde::{Deserialize, Serialize};
use std::collections::HashSet;
Expand All @@ -9,6 +10,8 @@ use syn::spanned::Spanned;
use syn::visit::{self, Visit};
use syn::{parse_str, Fields, File, Item, Meta, Type};

pub use rules::{Rule, RuleRegistry, RuleViolation, Severity};

#[cfg(not(target_arch = "wasm32"))]
use soroban_sdk::Env;
use thiserror::Error;
Expand Down Expand Up @@ -835,6 +838,10 @@ impl Analyzer {
_ => 8,
}
}

pub fn scan_deprecated_host_fns(&self, source: &str) -> Vec<RuleViolation> {
crate::rules::deprecated_host_fns::DeprecatedHostFnRule::new().check(source)
}
}

// ── UnsafeVisitor ─────────────────────────────────────────────────────────────
Expand Down
118 changes: 118 additions & 0 deletions tooling/sanctifier-core/src/rules/deprecated_host_fns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use syn::visit::{self, Visit};
use syn::{parse_str, Expr, ExprMethodCall, File};
use crate::rules::{Rule, RuleViolation, Severity};

pub struct DeprecatedHostFnRule;

impl DeprecatedHostFnRule {
pub fn new() -> Self {
Self
}
}

impl Rule for DeprecatedHostFnRule {
fn name(&self) -> &str {
"deprecated_host_fns"
}

fn check(&self, source: &str) -> Vec<RuleViolation> {
let file = match parse_str::<File>(source) {
Ok(f) => f,
Err(_) => return vec![],
};

let mut visitor = DeprecatedHostFnVisitor {
violations: Vec::new(),
};
visitor.visit_file(&file);
visitor.violations
}

fn as_any(&self) -> &dyn std::any::Any {
self
}
}

struct DeprecatedHostFnVisitor {
violations: Vec<RuleViolation>,
}

impl<'ast> Visit<'ast> for DeprecatedHostFnVisitor {
fn visit_expr_method_call(&mut self, i: &'ast ExprMethodCall) {
let method_name = i.method.to_string();

// List of deprecated Soroban host functions (v20+)
let deprecated_methods = [
("get_ledger_version", "env.ledger().version()"),
("get_ledger_sequence", "env.ledger().sequence()"),
("get_ledger_timestamp", "env.ledger().timestamp()"),
("get_current_contract_address", "env.current_contract_address()"),
("get_invoking_contract_address", "env.invoker()"),
];

for (deprecated, suggestion) in deprecated_methods {
if method_name == deprecated {
self.violations.push(RuleViolation {
rule_name: "deprecated_host_fns".to_string(),
severity: Severity::Warning,
message: format!(
"Usage of deprecated host function `{}`. Use `{}` instead.",
deprecated, suggestion
),
location: format!("line:{}", i.method.span().start().line),
});
}
}

visit::visit_expr_method_call(self, i);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_deprecated_host_fn_detection() {
let source = r#"
#[contractimpl]
impl MyContract {
pub fn test_fn(env: Env) {
let version = env.get_ledger_version();
let seq = env.get_ledger_sequence();
let ts = env.get_ledger_timestamp();
let addr = env.get_current_contract_address();
}
}
"#;

let rule = DeprecatedHostFnRule::new();
let issues = rule.check(source);

assert_eq!(issues.len(), 4);
assert!(issues[0].message.contains("get_ledger_version"));
assert!(issues[1].message.contains("get_ledger_sequence"));
assert!(issues[2].message.contains("get_ledger_timestamp"));
assert!(issues[3].message.contains("get_current_contract_address"));
}

#[test]
fn test_no_deprecated_calls() {
let source = r#"
#[contractimpl]
impl MyContract {
pub fn test_fn(env: Env) {
let version = env.ledger().version();
let seq = env.ledger().sequence();
let ts = env.ledger().timestamp();
let addr = env.current_contract_address();
}
}
"#;

let rule = DeprecatedHostFnRule::new();
let issues = rule.check(source);

assert_eq!(issues.len(), 0);
}
}
54 changes: 54 additions & 0 deletions tooling/sanctifier-core/src/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
pub mod deprecated_host_fns;

use serde::{Deserialize, Serialize};
use std::any::Any;

pub trait Rule: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe {
fn name(&self) -> &str;
fn check(&self, source: &str) -> Vec<RuleViolation>;
fn as_any(&self) -> &dyn Any;
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleViolation {
pub rule_name: String,
pub severity: Severity,
pub message: String,
pub location: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
#[default]
Info,
Warning,
Error,
}

pub struct RuleRegistry {
rules: Vec<Box<dyn Rule>>,
}

impl RuleRegistry {
pub fn new() -> Self {
Self { rules: Vec::new() }
}

pub fn register(&mut self, rule: Box<dyn Rule>) {
self.rules.push(rule);
}

pub fn run_all(&self, source: &str) -> Vec<RuleViolation> {
self.rules
.iter()
.flat_map(|rule| rule.check(source))
.collect()
}

pub fn with_default_rules() -> Self {
let mut registry = Self::new();
registry.register(Box::new(deprecated_host_fns::DeprecatedHostFnRule::new()));
registry
}
}
Loading