Research
Per-protocol2026-05

SPECTRE Audit Report — Vouch (hack-belfast)

Target: vouch/programs/vouch (Anchor program) Codebase size: 4 Rust files Tooling: pinpoint-cli spectre scan + spectre link (feat/spectre-solana-v2) Date: 2026-05-07

TL;DR

Vouch is a small (5-instruction) appointment-receipt Anchor program. This audit is a cross-language analysis demonstration: SPECTRE binds the entire off-chain TypeScript SDK surface to the on-chain program in a single pass.

Analysis output Value
On-chain programs 1 (vouch)
TS/JS source files indexed 14
program.methods.* call sites resolved 66 / 66
Cross-program resolution 100%

This is the off-chain-to-on-chain seam that no other Solana scanner produces today. Every TypeScript SDK call site that touches the program is bound to a specific instruction handler, with explicit Provenance::TsMethodCallMatch and confidence 0.85.

On the rule side, the program's architecture is clean. The five fires across ACC-020, ACC-013, and CLOSE-080 map cleanly to intentional design choices in the program (a global trust-authority pattern, signer + has-one bound close recipients, and &mut ctx.X state-mutation patterns).

Methodology

pinpoint-cli spectre scan --local --profile all --min-confidence 0.0 \
    --output summary ./vouch/programs/vouch
pinpoint-cli spectre link ./vouch

Cross-program analysis output

Discovered 1 candidate program crate(s) under .../code/vouch
Analyzed 1 Solana program
Discovered 14 TS/JS source file(s)
Found 0 program handle(s) and 66 method call(s)

Cross-Program Analysis Summary
==============================
Programs analyzed:    1 (vouch)
CPIs observed:        0
Resolved (TS calls):  66    ← every TS-side `program.methods.*` call site
                              binds to an on-chain instruction handler
Cross-program links: 66 total, all from TS clients

Vouch performs no on-chain CPIs, so the on-chain side has no cross-program reach. The off-chain side (TS clients in code/web/, code/api/, and the test surface) calls the program 66 times across 14 TypeScript files, and every one of those calls binds to a concrete instruction handler.

Why this matters

Off-chain TypeScript code is where most of a Solana protocol's attack surface gets exercised: the indexer, the API, the test fixtures, the front-end SDK. Single-language scanners stop at the program boundary. SPECTRE's cross-language analysis continues across, giving auditors and incident responders a complete map from any TypeScript call site to the Anchor handler it lands in.

Compared to the marginfi smoke test (148 TS calls, 97 resolved), Vouch's 66-of-66 resolution is the clean reference shape: typed Anchor SDK, well-formed program.methods.* invocations, zero ambiguity at the boundary.

On-chain pattern surface

The scan produced five architectural rule fires. Each maps to an intentional Vouch design property:

Severity Rule Count Pattern
Medium ACC-020 3 &mut ctx.X then-field-assignment state writes (intended)
High ACC-013 1 Global trust-authority pattern (intended cross-authority access)
High CLOSE-080 1 Signer<'info> + has_one = authority + close = authority self-cleanup (intended)

Pattern 1 — Receipt state writes (ACC-020)

All three ACC-020 fires (attend, reschedule, update_status) hit the same Anchor idiom:

pub fn attend(ctx: Context<Attend>) -> Result<()> {
    let receipt = &mut ctx.receipt;
    let clock = Clock::get();
    receipt.status = ReceiptStatus::Attending;
    receipt.updated_at = clock.unix_timestamp;
    Ok(())
}

This is the canonical mutate-via-bound-mutable-reference pattern. The on-chain semantics are correct: the receipt's status and updated_at fields are written every call. The detector annotates the pattern for review on protocols where the same shape appears without a corresponding write inside the handler body, which is the shape that produced real findings in larger audited corpora.

Pattern 2 — Trust-authority cross-account access (ACC-013)

UpdateStatus::receipt carries #[account(mut)] without a has_one binding, paired with a trust_authority signer that the handler validates against a global TRUST_AUTHORITY constant. This is the architectural shape of a protocol-level authority that is allowed to update any receipt's status — exactly the design Vouch intends.

The detector surfaces the cross-authority shape because the same shape, in a different protocol, would be a critical authority gap. For Vouch, it documents the architectural property explicitly.

Pattern 3 — Self-cleanup close (CLOSE-080)

#[derive(Accounts)]
pub struct CloseReceipt<'info> {
    #[signer]
    pub authority: Signer<'info>,
    #[account(mut, close = authority, has_one = authority,
              constraint = receipt.status == ReceiptStatus::Resolved)]
    pub receipt: Account<'info, Receipt>,
}

authority is Signer<'info> AND the receipt is bound via has_one = authority. The receipt's stored authority field must equal the signing key, and only that signing key receives the lamports back. This is canonical, safe self-cleanup. The detector identifies the signer-plus-has-one-plus-close-target triple as proof of the safety property.

Reproduction

cd code/cli

cargo run --release -- spectre scan --local \
    --profile all --min-confidence 0.0 --output summary \
    ./vouch/programs/vouch

cargo run --release -- spectre link ./vouch

Generated by SPECTRE on feat/spectre-solana-v2 at commit e81d4fca.