Bulwark
The first automated auditor for Solana that combines Rust-native AST parsing with encrypted on-chain reputation.
Architecture Overview
A tri-layer system with a deliberate split of authority. A Rust binary built on the `syn` crate parses the AST and computes the 23 deterministic factors, and it alone owns the score. A NestJS orchestration layer stages GitHub workspaces, drives the Rust service over a local HTTP interface, and runs a separate GPT-4o pass for the qualitative dimensions static analysis cannot reach. An Anchor program on Solana stores the audit record, with pricing fields in plaintext for public verification and the commit hash encrypted client-side through Arcium.
The Challenges
Problem
We initially built the analyzer in Node.js using Regex, but it failed catastrophically on Anchor's heavy use of Rust macros (e.g., `#[derive(Accounts)]`). That regex baseline ran at a 40%+ false positive rate, because Regex could not understand the semantic relationship between nested structs and instruction handlers.
Solution
We scrapped the Node.js engine and rewrote the core in Rust using the `syn` crate. We implemented a Visitor Pattern to traverse the raw Abstract Syntax Tree. This allowed us to semantically parse macro expansions and control flow, dropping false positives to near zero and enabling deep dependency graph analysis.
analyzer/src/visitor.rs
Problem
With an LLM in the loop the tempting design is to let it grade the code, because it returns confident, well-written severity judgements that read like an auditor wrote them. It also makes the headline number irreproducible: the same commit analysed twice yields two different scores, and a client disputing a quote has nothing to check it against.
Solution
The two passes are structurally separated so they cannot contaminate each other. The score is derived purely from the deterministic AST factors (four sub-scores, structural, security, systemic and economic, combined on fixed weights of 0.2 / 0.3 / 0.3 / 0.2), and the audit estimate is a function of that number alone. The GPT-4o pass runs six independent analyses in parallel via `tokio::join!` (documentation clarity, testing coverage, financial-logic intricacy, profit attack vectors, value at risk, and code hotspots), each falling back to a default metric set if its call fails. Of all of that, only the hotspot list reaches the report. The model can point an auditor at a file; it cannot move the score or the quote.
analyzer/src/score_calculator.rs (calculate_total_score) / analyzer/src/ai_analysis.rs
Problem
The AST rewrite killed the macro-parsing false positives, but a second class survived it. Flagging every division and modulo in a crate also flags helper functions, test fixtures and dead utility code that no instruction can actually reach. That noise is worse than a missed finding, because it teaches an auditor to skim past the output.
Solution
We made the factors handler-centric rather than file-centric. Each analyser builds a call graph across the crate, identifies genuine Anchor instruction handlers through a signature heuristic (`is_anchor_handler`), then runs a depth-first traversal from every handler to compute the reachable function set. Arithmetic, CPI and access-control findings are counted only inside that set, so risky math in a helper nothing calls contributes zero. Reachability does the filtering, not a hand-maintained list of exceptions.
analyzer/src/factors/arithmetic.rs (find_reachable_functions)
Problem
Turning a complexity score into a dollar figure is the part clients actually argue about. Linear pricing fails because audit difficulty scales with how entangled the code is, not with line count, and any single point estimate invites a dispute the moment the audit runs long.
Solution
A piecewise linear interpolation over five calibration points taken from completed audits, mapping median complexity (16.98 through 50.62) onto audit timeline (8 through 30 days). Each point carries both a median and a skewed timeline, so the engine emits a band rather than a number. A phi-weighted multiplier (`1 - 0.8 + 0.8 * 2/3`) converts the two-auditor timeline into a three-auditor one, modelling the fact that adding auditors does not divide the work cleanly, and cost falls out as weeks multiplied by a $2,500–$6,000 weekly rate.
analyzer/src/score_calculator.rs (estimated_audit_effort)
Problem
Five calibration points is a thin dataset, and they only span complexity 16.98 to 50.62. Any repository scoring outside that window falls off the end of the interpolation entirely.
Solution
Calling this solved would be dishonest. Outside the calibrated range the engine falls back to a hardcoded slope (1.468429 above, 0.627746 below) fitted to nothing in particular, so a genuinely large protocol still receives a confident-looking quote with no data behind it. Shipping it that way was a deliberate tradeoff to get the pipeline working end to end, but it is the weakest link in the product and the first thing I would replace. The fix is not a better curve, it is more calibration points from completed audits, plus surfacing a confidence indicator when a score lands outside the calibrated band instead of silently extrapolating.
analyzer/src/score_calculator.rs (SLOPE_ABOVE_MAX / SLOPE_BELOW_MIN)
Problem
An audit certificate has to prove two things that pull against each other. The pricing must be public, so a client can verify the quote was not retro-fitted after the fact. The specific commit audited must stay private, because a repository identifier next to a low security score is a map for an attacker.
Solution
We split the on-chain `AuditRecord` by field instead of encrypting it wholesale. The pricing fields (report id, day range, auditor count, USD range and score) are stored in plaintext and readable by anyone on Solscan. The commit hash is encrypted client-side using Arcium's RescueCipher over an x25519 shared secret and stored as an opaque 32-byte field, with a separate Arcium computation that re-encrypts it for a nominated receiver so a client can grant a third party access without the platform ever publishing it. The limit is worth stating plainly: the encrypted circuit is wiring, not production cryptography. `share_commit_hash` merges two bytes into a u16 rather than operating over the full 32-byte array. That is enough to prove the Arcium integration end to end, not enough to describe the commit hash as MPC-protected in front of a real client.
program/programs/bulwark_storage/src/lib.rs / program/encrypted-ixs/src/lib.rs
Problem
Three runtimes (a Rust binary, a NestJS backend and a hosted model API) each fail in a different way. A large monorepo can push the Rust pass past any sensible request timeout, and the naive design of failing the job means the biggest and most valuable repositories are precisely the ones that never produce a report.
Solution
A shared-workspace architecture where NestJS owns git cloning and file staging, then invokes the Rust analyser over a local HTTP interface (`/augment`) under a configurable `RUST_ANALYZER_TIMEOUT` that defaults to five minutes. On timeout or connection failure the orchestrator logs the reason and proceeds without the augmentation rather than aborting, so the client still receives a report built from whatever did complete. The same principle runs one layer down, where each of the six AI sub-analyses falls back to a default metric set on error instead of taking the whole batch with it.
backend/src/static-analysis/static-analysis.service.ts