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
77 changes: 77 additions & 0 deletions .github/workflows/code-scanning-sanctifier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Code Scanning (Sanctifier + CodeQL)

# Uploads Sanctifier's SARIF to GitHub Code Scanning and runs CodeQL alongside
# it. Both tools publish into the same Code Scanning view using distinct
# `category` values so their alerts never overwrite each other.

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
# Re-scan weekly so newly published rules/queries surface on existing code.
- cron: "0 6 * * 1"

# Least-privilege defaults. `security-events: write` is required to upload SARIF.
permissions:
contents: read
security-events: write

jobs:
# ── Sanctifier: Soroban-specific static analysis → SARIF ────────────────────
sanctifier:
name: Sanctifier (Soroban)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

# Use the published composite action. It runs
# sanctifier analyze <path> --format sarif --min-severity <sev> --exit-code
# writes SARIF to `sarif-output`, and uploads it to Code Scanning.
- name: Run Sanctifier
uses: HyperSafeD/Sanctifier@main
continue-on-error: true # keep the workflow green; review alerts in the UI
with:
path: .
format: sarif
min-severity: high
upload-sarif: "true"
sarif-output: sanctifier-results.sarif

# ── CodeQL: GitHub's first-party analysis, runs alongside Sanctifier ────────
codeql:
name: CodeQL
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
strategy:
fail-fast: false
matrix:
# CodeQL supports JS/TS via the `frontend/` dashboard in this repo.
# Adjust the language list to match what you ship.
language: ["javascript-typescript"]
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Initialize CodeQL
uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
with:
languages: ${{ matrix.language }}

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
with:
category: "/language:${{ matrix.language }}"
31 changes: 31 additions & 0 deletions .github/workflows/contracts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,37 @@ jobs:
- name: cargo check --workspace
run: cargo check --workspace --locked

# -------------------------------------------------------------------------
# SEP-41 compliance: one standardized conformance suite that every SEP-41
# token contract must pass. Its dependency tree is `syn`-only (no Z3), so
# this job stays fast and self-contained.
# -------------------------------------------------------------------------
sep41-compliance:
name: SEP-41 compliance suite
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo artifacts
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-sep41-compliance-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-sep41-compliance-

- name: Run SEP-41 compliance suite
run: cargo test -p sep41-compliance

# -------------------------------------------------------------------------
# Job 1: check + clippy + test for every contract in the workspace.
# Uses fail-fast: false so a single failure surfaces all broken contracts.
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,47 @@ jobs:

- name: Run Z3 module boundary integration tests
run: cargo test --test smt_module_boundaries_test -p sanctifier-core

# ---------------------------------------------------------------------------
# Property-based tests — the analysis engine must not panic on any input.
# Generates random Rust source (structured Soroban contracts, free-form
# snippets, and arbitrary text) and runs every rule against it, asserting the
# result is always Ok(findings) or Err(parse_error). Runs as a SEPARATE job
# with a hard 60-second budget on the proptest run itself (10,000 cases).
# ---------------------------------------------------------------------------
proptest:
name: Property-Based Tests (proptest)
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Install Z3
run: |
sudo apt-get update
sudo apt-get install -y libz3-dev libdbus-1-dev libudev-dev pkg-config

- name: Cache cargo registry & build artifacts
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-proptest-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-proptest-

# Compile separately so the 60-second budget below covers test execution
# only, not the build.
- name: Build property-test binary (release)
run: cargo test -p sanctifier-core --test proptest_analysis --release --no-run

- name: Run property tests — 10,000 inputs, 60s budget
env:
PROPTEST_CASES: "10000"
run: timeout 60s cargo test -p sanctifier-core --test proptest_analysis --release
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ members = [
"contracts/vesting",
"contracts/benchmark",
"contracts/deposit-withdraw",
"contracts/sep41-compliance",

]
resolver = "2"
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ Every finding has a stable code — `S001..S012` — so you can filter, suppress

| Code | What it catches | Why it bites |
|------|-----------------|--------------|
| `S001` | Missing `require_auth` on state-changing calls | Anyone can drain your contract |
| `S002` | `panic!` / `unwrap` / `expect` in contract paths | Locked state, no recovery |
| `S003` | Unchecked arithmetic — overflow, underflow, truncation | Silent loss-of-funds rounding |
| `S004` | Ledger entries pushing the size threshold | Refusal at write time, mid-tx |
| `S005` | Storage-key collisions between data paths | Cross-feature data corruption |
| `S006` | Unsafe patterns — including timestamp-as-randomness | Predictable winners, exploit replay |
| `S007` | Your custom YAML rules | Your house style, enforced |
| `S008` | Inconsistent or missing event emissions | Wallets and indexers go blind |
| `S009` | Unhandled `Result` return values | Silent failures masquerading as success |
| `S010` | Upgrade / admin / governance risk | Single-key takeover paths |
| `S011` | Z3-disproved invariants | Mathematical guarantees you don't have |
| `S012` | SEP-41 token interface deviations | Wallets reject your token |
| [`S001`](docs/rules/S001.md) | Missing `require_auth` on state-changing calls | Anyone can drain your contract |
| [`S002`](docs/rules/S002.md) | `panic!` / `unwrap` / `expect` in contract paths | Locked state, no recovery |
| [`S003`](docs/rules/S003.md) | Unchecked arithmetic — overflow, underflow, truncation | Silent loss-of-funds rounding |
| [`S004`](docs/rules/S004.md) | Ledger entries pushing the size threshold | Refusal at write time, mid-tx |
| [`S005`](docs/rules/S005.md) | Storage-key collisions between data paths | Cross-feature data corruption |
| [`S006`](docs/rules/S006.md) | Unsafe patterns — including timestamp-as-randomness | Predictable winners, exploit replay |
| [`S007`](docs/rules/S007.md) | Your custom YAML rules | Your house style, enforced |
| [`S008`](docs/rules/S008.md) | Inconsistent or missing event emissions | Wallets and indexers go blind |
| [`S009`](docs/rules/S009.md) | Unhandled `Result` return values | Silent failures masquerading as success |
| [`S010`](docs/rules/S010.md) | Upgrade / admin / governance risk | Single-key takeover paths |
| [`S011`](docs/rules/S011.md) | Z3-disproved invariants | Mathematical guarantees you don't have |
| [`S012`](docs/rules/S012.md) | SEP-41 token interface deviations | Wallets reject your token |

Plus the community **vulnerability database** matches known CVE-style patterns (`SOL-2024-*`) against your AST — so a published exploit anywhere becomes a finding everywhere.

Expand Down
18 changes: 18 additions & 0 deletions contracts/sep41-compliance/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "sep41-compliance"
version = "0.1.0"
edition = "2021"
publish = false
description = "Standardized SEP-41 token-interface compliance suite run against every Sanctifier token contract."

# This crate carries no runtime library of its own — it exists purely to host
# the `cargo test -p sep41-compliance` conformance suite. The reusable harness
# lives in `sanctifier-test-support::sep41_compliance`.
[lib]
path = "src/lib.rs"

[dependencies]
sanctifier-test-support = { workspace = true }

[dev-dependencies]
sanctifier-test-support = { workspace = true }
32 changes: 32 additions & 0 deletions contracts/sep41-compliance/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! # `sep41-compliance` — the standard SEP-41 conformance suite
//!
//! Every SEP-41 token contract in this workspace must pass one shared,
//! standardized compliance suite rather than relying on per-contract, ad-hoc
//! tests. This crate is that suite's home.
//!
//! - The reusable, generic harness lives in
//! [`sanctifier_test_support::sep41_compliance`]. It takes a contract's
//! source and verifies all ten SEP-41 functions: presence, exact signature,
//! and caller authorization.
//! - The actual conformance tests live in `tests/compliance.rs` and run the
//! harness against several real and reference token contracts.
//!
//! Run the whole suite with:
//!
//! ```bash
//! cargo test -p sep41-compliance
//! ```
//!
//! Adding a new token contract to the suite is one line in
//! `tests/compliance.rs`:
//!
//! ```rust,ignore
//! assert_compliant("my-new-token", include_str!("../../my-new-token/src/lib.rs"));
//! ```
//!
//! Any deviation from the SEP-41 interface — a missing function, a wrong
//! parameter type, or a missing `require_auth` — fails the suite.

/// Re-export of the conformance harness so downstream crates can depend on a
/// single, stable path (`sep41_compliance::assert_compliant`).
pub use sanctifier_test_support::sep41_compliance;
103 changes: 103 additions & 0 deletions contracts/sep41-compliance/tests/compliance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! Standardized SEP-41 compliance suite.
//!
//! This is the single, shared conformance suite that every SEP-41 token in the
//! workspace must pass. It runs the generic harness in
//! `sanctifier_test_support::sep41_compliance` against each token's source.
//!
//! Run it with:
//!
//! ```bash
//! cargo test -p sep41-compliance
//! ```
//!
//! ## Adding a contract
//!
//! Add one `assert_compliant(...)` line in [`compliant_contracts`] pointing at
//! the contract's source. The harness verifies all ten SEP-41 functions —
//! presence, exact signature, and caller authorization — and any deviation
//! fails the test.

use sanctifier_test_support::sep41_compliance::{
assert_compliant, assert_deviates, check, IssueKind, REQUIRED_FUNCTION_COUNT,
};

/// Every contract here is asserted to be a fully compliant SEP-41 token.
///
/// The suite intentionally covers **more than three** token contracts:
///
/// 1. `my-contract` — a real, in-repo SEP-41 token implementation.
/// 2. `amm-lp-token` — reference LP token an `amm-pool` mints for liquidity
/// shares (`contracts/amm-pool`).
/// 3. `deposit-receipt-token` — reference receipt token a `deposit-withdraw`
/// vault mints for depositor claims (`contracts/deposit-withdraw`); also
/// exercises the `require_auth_for_args` authorization variant.
fn compliant_contracts() -> Vec<(&'static str, &'static str)> {
vec![
("my-contract", include_str!("../../my-contract/src/lib.rs")),
("amm-lp-token", include_str!("fixtures/amm_lp_token.rs")),
(
"deposit-receipt-token",
include_str!("fixtures/deposit_receipt_token.rs"),
),
]
}

#[test]
fn suite_covers_at_least_three_contracts() {
assert!(
compliant_contracts().len() >= 3,
"the SEP-41 compliance suite must run against at least 3 contracts"
);
}

#[test]
fn sep41_interface_has_ten_functions() {
assert_eq!(REQUIRED_FUNCTION_COUNT, 10);
}

/// Every compliant contract passes the full SEP-41 conformance check.
#[test]
fn all_token_contracts_are_sep41_compliant() {
for (name, source) in compliant_contracts() {
assert_compliant(name, source);
}
}

// ── Negative tests: any deviation must fail ─────────────────────────────────
//
// These prove the suite is not vacuously green — a contract that drifts from
// the SEP-41 interface is reliably caught.

#[test]
fn missing_function_fails_compliance() {
assert_deviates(
"noncompliant-missing-function",
include_str!("fixtures/noncompliant_missing_function.rs"),
IssueKind::MissingFunction,
);
}

#[test]
fn wrong_signature_fails_compliance() {
assert_deviates(
"noncompliant-wrong-signature",
include_str!("fixtures/noncompliant_wrong_signature.rs"),
IssueKind::SignatureMismatch,
);
}

#[test]
fn missing_authorization_fails_compliance() {
assert_deviates(
"noncompliant-missing-auth",
include_str!("fixtures/noncompliant_missing_auth.rs"),
IssueKind::AuthorizationMismatch,
);
}

/// A non-token contract is never reported as a compliant SEP-41 token.
#[test]
fn non_token_contract_is_not_compliant() {
let report = check("pub struct C; impl C { pub fn ping(e: Env) -> u32 { 0 } }");
assert!(!report.compliant);
}
Loading
Loading