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/README.md +73 -15
- package/dist/chunk-BZVZ3WAU.js +1659 -0
- package/dist/cli.js +68 -2
- package/dist/index.d.ts +200 -2
- package/dist/index.js +39 -1
- package/dist/programs/directory.yaml +179 -0
- package/dist/rules/anchor-init-if-needed-guarded.yaml +47 -0
- package/dist/rules/bags-fee-share-creator-included.yaml +6 -1
- package/dist/rules/metaplex-metadata-immutable.yaml +55 -0
- package/dist/rules/privy-jwt-verification.yaml +5 -1
- package/dist/rules/token-2022-program-id-pinned.yaml +55 -0
- package/package.json +50 -9
- package/dist/chunk-H2Y75CSH.js +0 -494
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-
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|