brainblast 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,22 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ analyzeCosts,
3
4
  audit,
5
+ buildTrustGraph,
6
+ cacheSize,
7
+ defaultCachePath,
8
+ isValidSolanaAddress,
9
+ loadProgramCache,
10
+ renderCostReportMd,
11
+ renderTrustGraphMd,
4
12
  resolveRules
5
- } from "./chunk-H2Y75CSH.js";
13
+ } from "./chunk-BZVZ3WAU.js";
6
14
 
7
15
  // src/cli.ts
8
16
  import { writeFileSync, mkdirSync } from "fs";
9
17
  import { join } from "path";
10
18
  var args = process.argv.slice(2);
19
+ if (args[0] === "trust-graph") {
20
+ await runTrustGraph(args.slice(1));
21
+ process.exit(0);
22
+ }
11
23
  var ci = args.includes("--ci");
12
24
  var strict = args.includes("--strict");
13
25
  var targetDir = args.find((a) => !a.startsWith("--")) ?? process.cwd();
14
26
  var rules = resolveRules(targetDir);
15
27
  var { checks, report } = audit(targetDir, rules);
28
+ var costReport = analyzeCosts(targetDir);
29
+ report.costAnalysis = costReport;
16
30
  var outDir = join(targetDir, ".agent-research");
17
31
  mkdirSync(outDir, { recursive: true });
18
32
  var reportPath = join(outDir, "report.json");
19
33
  writeFileSync(reportPath, JSON.stringify(report, null, 2));
34
+ var costMdPath = join(outDir, "cost-analysis.md");
35
+ writeFileSync(costMdPath, renderCostReportMd(costReport));
20
36
  console.log(`brainblast: scanned ${targetDir} with ${rules.length} rule(s)`);
21
37
  if (checks.length === 0) console.log(" (no catastrophic components detected)");
22
38
  for (const c of checks) {
@@ -30,8 +46,58 @@ console.log(` verdict: ${report.summary.verdict} (fail=${fails}, cant_tell=${c
30
46
  if (cantTell > 0 && !strict) {
31
47
  console.log(` warning: ${cantTell} cant_tell (not gating \u2014 pass --strict to fail on these)`);
32
48
  }
33
- console.log(` report: ${reportPath}`);
49
+ console.log("\n\u2500\u2500 Cost & Rent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
50
+ if (!costReport.priorityFee.found) {
51
+ console.log(" [HIGH ] priority fee not configured \u2014 add setComputeUnitPrice to critical paths");
52
+ }
53
+ if (costReport.accountFlows.length === 0) {
54
+ console.log(" (no account-creation flows from tracked modules detected)");
55
+ } else {
56
+ for (const f of costReport.accountFlows) {
57
+ const file = f.file.split("/").slice(-2).join("/");
58
+ const scaleMark = f.scalable ? " [SCALABLE]" : "";
59
+ console.log(` ${f.accountType}${scaleMark} ${file}:${f.line} +${f.lamports.toLocaleString()} lamports (${f.sol} SOL)`);
60
+ }
61
+ if (costReport.totalLockupLamports > 0) {
62
+ console.log(` \u2500\u2500\u2500 static lockup total: ${costReport.totalLockupLamports.toLocaleString()} lamports (~${costReport.totalLockupSol} SOL)`);
63
+ }
64
+ }
65
+ console.log(` cost report: ${costMdPath}`);
66
+ console.log(` report: ${reportPath}`);
34
67
  if (ci) {
35
68
  const gateFail = fails > 0 || strict && cantTell > 0;
36
69
  process.exit(gateFail ? 1 : 0);
37
70
  }
71
+ async function runTrustGraph(argv) {
72
+ const rpcIdx = argv.indexOf("--rpc");
73
+ const rpcUrl = rpcIdx >= 0 ? argv[rpcIdx + 1] : void 0;
74
+ const noProbe = argv.includes("--no-probe");
75
+ const jsonOut = argv.includes("--json");
76
+ const ids = argv.filter((a) => !a.startsWith("--") && a !== rpcUrl);
77
+ if (ids.length === 0) {
78
+ console.error("usage: brainblast trust-graph <programId> [<programId>...] [--rpc URL] [--no-probe] [--json]");
79
+ process.exit(1);
80
+ }
81
+ for (const id of ids) {
82
+ if (!isValidSolanaAddress(id)) {
83
+ console.error(`error: '${id}' is not a valid Solana address`);
84
+ process.exit(1);
85
+ }
86
+ }
87
+ const noCache = argv.includes("--no-cache");
88
+ const graph = await buildTrustGraph(ids, {
89
+ rpcUrl,
90
+ probeRpc: !noProbe,
91
+ cachePath: noCache ? null : void 0
92
+ });
93
+ if (jsonOut) {
94
+ console.log(JSON.stringify(graph, null, 2));
95
+ } else {
96
+ console.log(renderTrustGraphMd(graph));
97
+ }
98
+ if (!noCache) {
99
+ const cp = defaultCachePath();
100
+ const count = cacheSize(loadProgramCache(cp));
101
+ console.error(` program-cache: ${count} entries (${cp})`);
102
+ }
103
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,43 @@
1
1
  import { FunctionDeclaration, ArrowFunction } from 'ts-morph';
2
2
 
3
+ declare function rentExemptMinimum(dataLen: number): number;
4
+ declare function lamportsToSol(lamports: number): string;
5
+ type Recoverability = "recoverable" | "non-recoverable" | "conditionally-recoverable";
6
+ interface AccountFlow {
7
+ call: string;
8
+ module: string;
9
+ accountType: string;
10
+ file: string;
11
+ line: number;
12
+ dataLen: number;
13
+ lamports: number;
14
+ sol: string;
15
+ recoverability: Recoverability;
16
+ recoverabilityNote: string;
17
+ /** Call appears inside a loop or .map()/.forEach() — cost scales with N */
18
+ scalable: boolean;
19
+ scalableNote?: string;
20
+ }
21
+ interface PriorityFeePosture {
22
+ /** true = setComputeUnitPrice call detected somewhere in the target */
23
+ found: boolean;
24
+ file?: string;
25
+ line?: number;
26
+ detail: string;
27
+ }
28
+ interface CostReport {
29
+ accountFlows: AccountFlow[];
30
+ priorityFee: PriorityFeePosture;
31
+ /** Sum of lamports across non-scalable flows (static lower bound) */
32
+ totalLockupLamports: number;
33
+ totalLockupSol: string;
34
+ /** Subset of flows that grow with N */
35
+ scalableFlows: AccountFlow[];
36
+ generatedAt: string;
37
+ }
38
+ declare function analyzeCosts(targetDir: string): CostReport;
39
+ declare function renderCostReportMd(r: CostReport): string;
40
+
3
41
  type Severity = "critical" | "high" | "medium" | "low";
4
42
  type CheckResultKind = "pass" | "fail" | "cant_tell";
5
43
  interface Candidate {
@@ -8,6 +46,34 @@ interface Candidate {
8
46
  params: string[];
9
47
  fn: FunctionDeclaration | ArrowFunction;
10
48
  }
49
+ interface RustAccountField {
50
+ /** Rust field identifier, e.g. "counter" */
51
+ name: string;
52
+ /** Raw type text, e.g. "Account<'info, Counter>", "Signer<'info>" */
53
+ typeName: string;
54
+ /** Full text of every #[account(...)] attribute on this field */
55
+ attrText: string;
56
+ /** Whether init_if_needed is present in attrText */
57
+ hasInitIfNeeded: boolean;
58
+ }
59
+ interface RustCandidate {
60
+ /** Source .rs file */
61
+ filePath: string;
62
+ /** Instruction handler name, e.g. "initialize" */
63
+ fnName: string;
64
+ /** Anchor Accounts struct name resolved from Context<X>, e.g. "Initialize" */
65
+ accountStructName: string;
66
+ /** All fields extracted from the Accounts struct */
67
+ accountFields: RustAccountField[];
68
+ /**
69
+ * Raw text of the instruction handler body block `{ ... }` — used by
70
+ * checkers that need to inspect companion calls (require!, data_is_empty, etc.)
71
+ * without a full tree-sitter traversal.
72
+ */
73
+ fnBodyText: string;
74
+ /** tree-sitter SyntaxNode for the function body — available for precise queries */
75
+ fnBodyNode: any;
76
+ }
11
77
  interface CheckOutcome {
12
78
  result: CheckResultKind;
13
79
  detail: string;
@@ -34,6 +100,8 @@ interface Rule {
34
100
  modules: string[];
35
101
  nameRegex: string;
36
102
  triggerCalls: string[];
103
+ /** Defaults to "typescript". Set to "rust" for Anchor/Rust checker kinds. */
104
+ lang?: "typescript" | "rust";
37
105
  };
38
106
  check: {
39
107
  kind: string;
@@ -96,6 +164,7 @@ declare function audit(targetDir: string, rules: Rule[]): {
96
164
  cant_tell: number;
97
165
  };
98
166
  openQuestions: never[];
167
+ costAnalysis: CostReport | null;
99
168
  };
100
169
  };
101
170
 
@@ -114,9 +183,138 @@ declare function renderTest(kind: string, opts: {
114
183
  }): string;
115
184
  declare const testKinds: string[];
116
185
 
117
- declare function runChecker(kind: string, c: Candidate, params: any): CheckOutcome;
186
+ declare function runChecker(kind: string, c: Candidate | RustCandidate, params: any): CheckOutcome;
118
187
  declare const checkerKinds: string[];
119
188
 
120
189
  declare function findCandidates(targetDir: string, rule: Rule): Candidate[];
121
190
 
122
- export { type Candidate, type CheckOutcome, type CheckResult, type CheckResultKind, type Rule, type Severity, audit, auditWithRule, rules as bundledRules, checkerKinds, findCandidates, generateTestForResult, loadRules, renderTest, resolveRules, runChecker, testKinds };
191
+ type UpgradeAuthorityKind = "renounced" | "single-key" | "multisig" | "dao" | "unknown";
192
+ type UpgradeAuthoritySource = "directory" | "rpc" | "research";
193
+ interface UpgradeAuthority {
194
+ kind: UpgradeAuthorityKind;
195
+ address: string | null;
196
+ source: UpgradeAuthoritySource;
197
+ checkedAt?: string;
198
+ }
199
+ type VerifiedBuildState = {
200
+ state: "verified";
201
+ registryUrl: string;
202
+ commit?: string;
203
+ } | {
204
+ state: "unverified";
205
+ } | {
206
+ state: "unknown";
207
+ };
208
+ interface AuditRef {
209
+ firm: string;
210
+ date: string;
211
+ reportUrl: string;
212
+ auditedCommit?: string;
213
+ }
214
+ interface ParityNote {
215
+ mainnet: "present" | "absent" | "different" | "unknown";
216
+ devnet: "present" | "absent" | "different" | "unknown";
217
+ testnet?: "present" | "absent" | "different" | "unknown";
218
+ notes?: string;
219
+ }
220
+ interface OnChainProgram {
221
+ programId: string;
222
+ name: string;
223
+ kind?: string;
224
+ upgradeAuthority: UpgradeAuthority;
225
+ verifiedBuild: VerifiedBuildState;
226
+ audits: AuditRef[];
227
+ parity: ParityNote;
228
+ invokes?: string[];
229
+ provenance?: {
230
+ directoryFile?: string;
231
+ rpcUrl?: string;
232
+ researchRun?: string;
233
+ notes?: string;
234
+ };
235
+ }
236
+ interface TrustGraph {
237
+ programs: OnChainProgram[];
238
+ unresolved: Array<{
239
+ programId: string;
240
+ reason: string;
241
+ }>;
242
+ generatedAt: string;
243
+ }
244
+
245
+ interface RpcOpts {
246
+ rpcUrl?: string;
247
+ timeoutMs?: number;
248
+ fetchImpl?: typeof fetch;
249
+ }
250
+
251
+ interface BuildOpts extends RpcOpts {
252
+ probeRpc?: boolean;
253
+ directoryPath?: string;
254
+ cachePath?: string | null;
255
+ }
256
+ declare function buildTrustGraph(programIds: string[], opts?: BuildOpts): Promise<TrustGraph>;
257
+
258
+ declare function renderTrustGraphMd(g: TrustGraph): string;
259
+
260
+ declare function loadDirectory(path?: string): Map<string, OnChainProgram>;
261
+
262
+ declare function base58Encode(bytes: Uint8Array): string;
263
+ declare function base58Decode(s: string): Uint8Array;
264
+ declare function isValidSolanaAddress(s: string): boolean;
265
+
266
+ declare const DEFAULT_TTL_HOURS = 168;
267
+ declare const SCHEMA_VERSION = "1.0";
268
+ interface ProgramCacheEntry {
269
+ program: OnChainProgram;
270
+ /** ISO 8601 timestamp of when this entry was written. */
271
+ cachedAt: string;
272
+ /** The run.id from the brainblast report that produced this entry. */
273
+ sourceRun: string;
274
+ /** Hours until this entry expires. Defaults to DEFAULT_TTL_HOURS. */
275
+ ttlHours?: number;
276
+ }
277
+ interface ProgramCache {
278
+ schemaVersion: typeof SCHEMA_VERSION;
279
+ entries: Record<string, ProgramCacheEntry>;
280
+ }
281
+ declare function defaultCachePath(): string;
282
+ /**
283
+ * Load the program cache from disk.
284
+ *
285
+ * Returns an empty cache if the file does not exist, is unreadable, or has an
286
+ * incompatible schemaVersion. Never throws on missing/corrupt files — callers
287
+ * can always start fresh.
288
+ */
289
+ declare function loadProgramCache(cachePath?: string): ProgramCache;
290
+ /**
291
+ * Persist the program cache to disk, creating parent directories as needed.
292
+ * Throws only on unrecoverable write errors (e.g. permission denied on the
293
+ * home directory itself).
294
+ */
295
+ declare function saveProgramCache(cache: ProgramCache, cachePath?: string): void;
296
+ /**
297
+ * Return the cached OnChainProgram for programId, or null if:
298
+ * - the entry does not exist
299
+ * - the entry has exceeded its TTL
300
+ *
301
+ * @param ttlHoursOverride - optional override for the TTL check (e.g. for tests)
302
+ */
303
+ declare function getCacheEntry(cache: ProgramCache, programId: string, ttlHoursOverride?: number): OnChainProgram | null;
304
+ /**
305
+ * Write (or overwrite) a cache entry for programId.
306
+ * Mutates the cache object in place and returns it for chaining.
307
+ */
308
+ declare function putCacheEntry(cache: ProgramCache, programId: string, program: OnChainProgram, sourceRun: string, ttlHours?: number): ProgramCache;
309
+ /**
310
+ * Return the raw ProgramCacheEntry for programId (including metadata), or null.
311
+ * Useful for rendering provenance to users ("cached 3 days ago by run X").
312
+ */
313
+ declare function getCacheEntryMeta(cache: ProgramCache, programId: string): ProgramCacheEntry | null;
314
+ declare function isEntryExpired(entry: ProgramCacheEntry, ttlHoursOverride?: number): boolean;
315
+ /**
316
+ * Count of non-expired entries in the cache (useful for status lines).
317
+ */
318
+ declare function cacheSize(cache: ProgramCache, ttlHoursOverride?: number): number;
319
+
320
+ export { type AccountFlow, type AuditRef, type BuildOpts, type Candidate, type CheckOutcome, type CheckResult, type CheckResultKind, type CostReport, DEFAULT_TTL_HOURS, type OnChainProgram, type ParityNote, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type Rule, type RustAccountField, type RustCandidate, type Severity, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, analyzeCosts, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, checkerKinds, defaultCachePath, findCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, isEntryExpired, isValidSolanaAddress, lamportsToSol, loadDirectory, loadProgramCache, loadRules, putCacheEntry, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, saveProgramCache, testKinds };
package/dist/index.js CHANGED
@@ -1,15 +1,34 @@
1
1
  import {
2
+ DEFAULT_TTL_HOURS,
3
+ analyzeCosts,
2
4
  audit,
3
5
  auditWithRule,
6
+ base58Decode,
7
+ base58Encode,
8
+ buildTrustGraph,
9
+ cacheSize,
4
10
  checkerKinds,
11
+ defaultCachePath,
5
12
  findCandidates,
13
+ getCacheEntry,
14
+ getCacheEntryMeta,
15
+ isEntryExpired,
16
+ isValidSolanaAddress,
17
+ lamportsToSol,
18
+ loadDirectory,
19
+ loadProgramCache,
6
20
  loadRules,
21
+ putCacheEntry,
22
+ renderCostReportMd,
7
23
  renderTest,
24
+ renderTrustGraphMd,
25
+ rentExemptMinimum,
8
26
  resolveRules,
9
27
  rules,
10
28
  runChecker,
29
+ saveProgramCache,
11
30
  testKinds
12
- } from "./chunk-H2Y75CSH.js";
31
+ } from "./chunk-BZVZ3WAU.js";
13
32
 
14
33
  // src/generate.ts
15
34
  import { writeFileSync, mkdirSync } from "fs";
@@ -25,15 +44,34 @@ function generateTestForResult(result, rule, outPath) {
25
44
  return outPath;
26
45
  }
27
46
  export {
47
+ DEFAULT_TTL_HOURS,
48
+ analyzeCosts,
28
49
  audit,
29
50
  auditWithRule,
51
+ base58Decode,
52
+ base58Encode,
53
+ buildTrustGraph,
30
54
  rules as bundledRules,
55
+ cacheSize,
31
56
  checkerKinds,
57
+ defaultCachePath,
32
58
  findCandidates,
33
59
  generateTestForResult,
60
+ getCacheEntry,
61
+ getCacheEntryMeta,
62
+ isEntryExpired,
63
+ isValidSolanaAddress,
64
+ lamportsToSol,
65
+ loadDirectory,
66
+ loadProgramCache,
34
67
  loadRules,
68
+ putCacheEntry,
69
+ renderCostReportMd,
35
70
  renderTest,
71
+ renderTrustGraphMd,
72
+ rentExemptMinimum,
36
73
  resolveRules,
37
74
  runChecker,
75
+ saveProgramCache,
38
76
  testKinds
39
77
  };
@@ -0,0 +1,179 @@
1
+ # Curated Solana program directory. Human-authored facts; never LLM-edited in
2
+ # place. Every entry is a program brainblast considers "known enough to
3
+ # describe without an RPC call." For anything not in this file, the
4
+ # trust-graph builder falls back to live RPC probing for upgrade authority and
5
+ # reports verifiedBuild=unknown / audits=[] unless the research skill
6
+ # enriches it.
7
+ #
8
+ # Add an entry only when you can cite a primary source for every field.
9
+ # Comments above each entry name those sources.
10
+
11
+ programs:
12
+ # System Program — owns wallets, transfers SOL, allocates accounts. Core
13
+ # runtime; not an upgradeable program. https://docs.solana.com/runtime/programs
14
+ - programId: "11111111111111111111111111111111"
15
+ name: System Program
16
+ kind: runtime
17
+ upgradeAuthority:
18
+ kind: renounced
19
+ address: null
20
+ source: directory
21
+ verifiedBuild:
22
+ state: verified
23
+ registryUrl: https://github.com/anza-xyz/agave
24
+ audits: []
25
+ parity:
26
+ mainnet: present
27
+ devnet: present
28
+ testnet: present
29
+ notes: Identical on every cluster; part of the validator binary.
30
+
31
+ # SPL Token — the legacy SPL token program. Native, non-upgradeable.
32
+ # https://spl.solana.com/token
33
+ - programId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
34
+ name: SPL Token
35
+ kind: token
36
+ upgradeAuthority:
37
+ kind: renounced
38
+ address: null
39
+ source: directory
40
+ verifiedBuild:
41
+ state: verified
42
+ registryUrl: https://github.com/solana-program/token
43
+ audits:
44
+ - firm: Kudelski Security
45
+ date: "2020-08-01"
46
+ reportUrl: https://github.com/solana-program/token/tree/main/audits
47
+ parity:
48
+ mainnet: present
49
+ devnet: present
50
+ testnet: present
51
+ notes: Same address everywhere. Distinct from Token-2022; consumers must pick one and never mix.
52
+
53
+ # SPL Token-2022 — the new token program with transfer hooks, fees, and
54
+ # confidential transfers. DIFFERENT program id from legacy Token; mixing the
55
+ # two is the #1 trap in the deep-dive research.
56
+ # https://spl.solana.com/token-2022
57
+ - programId: "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
58
+ name: SPL Token-2022
59
+ kind: token
60
+ upgradeAuthority:
61
+ kind: multisig
62
+ address: "9YBjtad6ZxR7hxNXyTjRRPnPipFbuAU6c2EVHTSVTQAW"
63
+ source: directory
64
+ checkedAt: "2025-04-15"
65
+ verifiedBuild:
66
+ state: verified
67
+ registryUrl: https://github.com/solana-program/token-2022
68
+ audits:
69
+ - firm: OtterSec
70
+ date: "2024-02-01"
71
+ reportUrl: https://github.com/solana-program/token-2022/tree/main/audits
72
+ parity:
73
+ mainnet: present
74
+ devnet: present
75
+ testnet: present
76
+ notes: >-
77
+ Same address on every cluster. Behavior identical, but extensions like transfer-fee
78
+ and transfer-hook can encode mainnet-only economics that don't show up in devnet
79
+ testing of a fresh mint.
80
+
81
+ # Metaplex Token Metadata. The metadata account for every NFT and most
82
+ # SPL-token branding goes through this program. isMutable / collection
83
+ # verification are immutable-after-mint decisions worth flagging.
84
+ # https://developers.metaplex.com/token-metadata
85
+ - programId: "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
86
+ name: Metaplex Token Metadata
87
+ kind: metadata
88
+ upgradeAuthority:
89
+ kind: multisig
90
+ address: "BFV9MAfDjT8azkAArh9k4iAjwboTBA9z3PNK7BFNKaiK"
91
+ source: directory
92
+ checkedAt: "2025-04-15"
93
+ verifiedBuild:
94
+ state: verified
95
+ registryUrl: https://github.com/metaplex-foundation/mpl-token-metadata
96
+ audits:
97
+ - firm: OtterSec
98
+ date: "2023-09-01"
99
+ reportUrl: https://github.com/metaplex-foundation/mpl-token-metadata/tree/main/programs/token-metadata/audits
100
+ parity:
101
+ mainnet: present
102
+ devnet: present
103
+ testnet: present
104
+ notes: Same address everywhere; metadata is the standard NFT/token-info layer.
105
+
106
+ # Memo Program. Lets transactions include UTF-8 notes; no state. Useful in
107
+ # CPI but trivially safe.
108
+ - programId: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
109
+ name: SPL Memo
110
+ kind: utility
111
+ upgradeAuthority:
112
+ kind: renounced
113
+ address: null
114
+ source: directory
115
+ verifiedBuild:
116
+ state: verified
117
+ registryUrl: https://github.com/solana-program/memo
118
+ audits: []
119
+ parity:
120
+ mainnet: present
121
+ devnet: present
122
+ testnet: present
123
+
124
+ # Compute Budget Program. Lets a tx set its compute unit price/limit. Pure
125
+ # runtime; not upgradeable. Worth knowing for fee posture in the cost report.
126
+ - programId: "ComputeBudget111111111111111111111111111111"
127
+ name: Compute Budget Program
128
+ kind: runtime
129
+ upgradeAuthority:
130
+ kind: renounced
131
+ address: null
132
+ source: directory
133
+ verifiedBuild:
134
+ state: verified
135
+ registryUrl: https://github.com/anza-xyz/agave
136
+ audits: []
137
+ parity:
138
+ mainnet: present
139
+ devnet: present
140
+ testnet: present
141
+
142
+ # Address Lookup Table Program. Lets transactions reference more accounts
143
+ # than the legacy 32-account limit allows.
144
+ - programId: "AddressLookupTab1e1111111111111111111111111"
145
+ name: Address Lookup Table Program
146
+ kind: runtime
147
+ upgradeAuthority:
148
+ kind: renounced
149
+ address: null
150
+ source: directory
151
+ verifiedBuild:
152
+ state: verified
153
+ registryUrl: https://github.com/anza-xyz/agave
154
+ audits: []
155
+ parity:
156
+ mainnet: present
157
+ devnet: present
158
+ testnet: present
159
+
160
+ # BPF Upgradeable Loader. The program that owns every upgradeable program;
161
+ # `setAuthority` and `upgrade` instructions go through here. The trust
162
+ # graph routinely reads ProgramData accounts owned by this address to
163
+ # resolve the upgrade authority of any other upgradeable program.
164
+ - programId: "BPFLoaderUpgradeab1e11111111111111111111111"
165
+ name: BPF Upgradeable Loader
166
+ kind: loader
167
+ upgradeAuthority:
168
+ kind: renounced
169
+ address: null
170
+ source: directory
171
+ verifiedBuild:
172
+ state: verified
173
+ registryUrl: https://github.com/anza-xyz/agave
174
+ audits: []
175
+ parity:
176
+ mainnet: present
177
+ devnet: present
178
+ testnet: present
179
+ notes: The loader itself is part of the validator and not upgradeable in the BPF sense.
@@ -0,0 +1,47 @@
1
+ # Pure-data rule (facts). Binds to vetted templates by `check.kind`/`test.kind`.
2
+ # Promoted from findings/anchor-init-if-needed-reinit.json after the
3
+ # proof-as-classifier loop (synth-prove) closed RED->GREEN against the
4
+ # new `anchor-init-if-needed-guarded` checker kind (tree-sitter-rust based).
5
+ id: anchor-init-if-needed-guarded
6
+ severity: critical
7
+ title: Anchor init_if_needed account has a reinitialization guard
8
+ component:
9
+ name: Anchor Framework
10
+ type: Blockchain
11
+ version: ">=0.26.0"
12
+ sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#init_if_needed
13
+ detect:
14
+ # This rule operates on Rust source files — requires tree-sitter-rust.
15
+ lang: rust
16
+ modules:
17
+ - "@coral-xyz/anchor"
18
+ - "@project-serum/anchor"
19
+ # Match instruction handlers that initialize accounts. Detection also
20
+ # triggers on any function whose Accounts struct has init_if_needed.
21
+ nameRegex: "initialize|init_if_needed"
22
+ triggerCalls:
23
+ - init_if_needed
24
+ check:
25
+ kind: anchor-init-if-needed-guarded
26
+ params:
27
+ passDetail: >-
28
+ Handler has #[account(init_if_needed)] and a reinitialization guard
29
+ (require!, data_is_empty, or is_initialized check). A second invocation
30
+ will be rejected before writing to the account.
31
+ failAbsentDetail: >-
32
+ Handler uses #[account(init_if_needed)] without a reinitialization guard.
33
+ A malicious caller can invoke the instruction a second time to overwrite
34
+ the account's data — a full state-wipe with no on-chain recourse.
35
+ Add: require!(account_field.field == initial_value, ErrorCode::AlreadyInitialized)
36
+ at the top of the handler body.
37
+ absentDetail: >-
38
+ Handler has no #[account(init_if_needed)] fields; the reinit-guard rule
39
+ does not apply.
40
+ test:
41
+ kind: anchor-program-test
42
+ params:
43
+ scenario: reinit-attempt
44
+ description: >-
45
+ Calls the instruction twice on the same account; the second call must
46
+ fail with AlreadyInitialized (or equivalent). If it succeeds, the
47
+ account state has been silently overwritten.
@@ -9,7 +9,12 @@ component:
9
9
  sourceUrl: https://docs.bags.fm/changelog/changelog
10
10
  detect:
11
11
  modules: ["@bagsfm/bags-sdk"]
12
- nameRegex: "feeshare|feeclaimer|bagsfee|launchtoken|tokenlaunch"
12
+ # Keep the regex tight to Bags-specific terms; broad terms like
13
+ # "launchtoken"/"tokenlaunch" cross-matched non-Bags Solana handlers
14
+ # (e.g. Token-2022 launch helpers) and produced spurious "no Bags fee
15
+ # config" failures. Bags candidates still resolve via the import or the
16
+ # createBagsFeeShareConfig trigger call.
17
+ nameRegex: "feeshare|feeclaimer|bagsfee"
13
18
  triggerCalls: [createBagsFeeShareConfig]
14
19
  check:
15
20
  kind: fee-allocation-shape