brainblast 0.7.3 → 0.7.5

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildTrustGraph
3
- } from "./chunk-SVSVVW6U.js";
3
+ } from "./chunk-GF37ANSW.js";
4
4
 
5
5
  // src/score.ts
6
6
  var W_AUTHORITY = 35;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ getAccountInfo,
2
3
  probeUpgradeAuthority
3
4
  } from "./chunk-XSVQSK53.js";
4
5
 
@@ -39,6 +40,41 @@ function loadDirectory(path = bundledPath()) {
39
40
  return m;
40
41
  }
41
42
 
43
+ // src/trustGraph/classifyAuthority.ts
44
+ var SYSTEM_PROGRAM = "11111111111111111111111111111111";
45
+ var KNOWN_AUTHORITY_OWNERS = {
46
+ // Squads — the dominant Solana multisig. v3 ("SMPL") and v4 program ids.
47
+ // https://docs.squads.so/main/development/sdk/program-ids
48
+ SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu: { kind: "multisig", label: "Squads v3" },
49
+ SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf: { kind: "multisig", label: "Squads v4" },
50
+ // SPL Governance (Realms) — the standard on-chain governance program.
51
+ // https://github.com/solana-labs/solana-program-library/tree/master/governance
52
+ GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw: { kind: "dao", label: "SPL Governance (Realms)" }
53
+ };
54
+ async function classifyUpgradeAuthority(address, opts = {}) {
55
+ const acct = await getAccountInfo(address, opts);
56
+ if (!acct) {
57
+ return { kind: "unknown" };
58
+ }
59
+ if (acct.owner === SYSTEM_PROGRAM) {
60
+ return { kind: "single-key", ownerProgram: SYSTEM_PROGRAM };
61
+ }
62
+ const known = KNOWN_AUTHORITY_OWNERS[acct.owner];
63
+ if (known) {
64
+ return { kind: known.kind, ownerProgram: acct.owner, ownerLabel: known.label };
65
+ }
66
+ return { kind: "unknown", ownerProgram: acct.owner };
67
+ }
68
+ async function enrichAuthorityClassification(authority, opts = {}) {
69
+ if (authority.kind !== "unknown" || !authority.address) return authority;
70
+ const c = await classifyUpgradeAuthority(authority.address, opts);
71
+ return {
72
+ ...authority,
73
+ kind: c.kind,
74
+ ...c.ownerProgram ? { ownerProgram: c.ownerProgram } : {}
75
+ };
76
+ }
77
+
42
78
  // src/trustGraph/programCache.ts
43
79
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
44
80
  import { join as join2, dirname } from "path";
@@ -146,6 +182,12 @@ async function buildTrustGraph(programIds, opts = {}) {
146
182
  let authority;
147
183
  try {
148
184
  authority = await probeUpgradeAuthority(id, opts);
185
+ if (opts.classifyAuthority !== false) {
186
+ try {
187
+ authority = await enrichAuthorityClassification(authority, opts);
188
+ } catch {
189
+ }
190
+ }
149
191
  } catch (e) {
150
192
  unresolved.push({ programId: id, reason: `rpc_error: ${e?.message ?? String(e)}` });
151
193
  continue;
@@ -174,6 +216,10 @@ async function buildTrustGraph(programIds, opts = {}) {
174
216
 
175
217
  export {
176
218
  loadDirectory,
219
+ SYSTEM_PROGRAM,
220
+ KNOWN_AUTHORITY_OWNERS,
221
+ classifyUpgradeAuthority,
222
+ enrichAuthorityClassification,
177
223
  DEFAULT_TTL_HOURS,
178
224
  defaultCachePath,
179
225
  loadProgramCache,
@@ -7,7 +7,7 @@ import {
7
7
  loadPack,
8
8
  resolveRules,
9
9
  walk
10
- } from "./chunk-5H5JQXXU.js";
10
+ } from "./chunk-V4XS5DKD.js";
11
11
 
12
12
  // src/costAnalysis.ts
13
13
  import { Project, SyntaxKind } from "ts-morph";
@@ -1,19 +1,27 @@
1
1
  // src/trustGraph/render.ts
2
2
  function renderAuthority(p) {
3
3
  const a = p.upgradeAuthority;
4
+ const owner = a.ownerProgram ? ` _(owner: \`${a.ownerProgram}\`)_` : "";
4
5
  switch (a.kind) {
5
6
  case "renounced":
6
7
  return "\u{1F512} **Renounced** \u2014 program is frozen; no key can upgrade it.";
7
8
  case "single-key":
8
- return `\u26A0\uFE0F **Single key** \`${a.address}\` \u2014 one private key can replace this program at any time.`;
9
+ return `\u26A0\uFE0F **Single key** \`${a.address}\` \u2014 one private key can replace this program at any time.${owner}`;
9
10
  case "multisig":
10
- return `\u{1F510} **Multisig** \`${a.address}\` \u2014 a threshold of signers can upgrade.`;
11
+ return `\u{1F510} **Multisig** \`${a.address}\` \u2014 a threshold of signers can upgrade.${owner}`;
11
12
  case "dao":
12
- return `\u{1F3DB} **DAO** \`${a.address}\` \u2014 governance program controls upgrades.`;
13
+ return `\u{1F3DB} **DAO** \`${a.address}\` \u2014 governance program controls upgrades.${owner}`;
13
14
  case "unknown":
14
- return a.address ? `\u2753 **Unclassified authority** \`${a.address}\` \u2014 needs research to confirm single-key vs multisig/DAO.` : "\u2753 **Unknown** \u2014 could not determine upgrade authority.";
15
+ return a.address ? `\u2753 **Unclassified authority** \`${a.address}\`${owner} \u2014 needs research to confirm single-key vs multisig/DAO.` : "\u2753 **Unknown** \u2014 could not determine upgrade authority.";
15
16
  }
16
17
  }
18
+ function renderTrustSummary(p) {
19
+ const a = p.upgradeAuthority;
20
+ const authBit = a.kind === "renounced" ? "\u{1F512} immutable" : a.kind === "multisig" ? "\u{1F510} multisig" : a.kind === "dao" ? "\u{1F3DB} DAO-governed" : a.kind === "single-key" ? "\u26A0\uFE0F single-key upgradeable" : "\u2753 authority unclassified";
21
+ const verifiedBit = p.verifiedBuild.state === "verified" ? "\u2705 verified build" : p.verifiedBuild.state === "unverified" ? "\u274C unverified" : "\u2753 build unchecked";
22
+ const auditBit = p.audits.length ? `\u2705 audited (${p.audits.map((x) => x.firm).join(", ")})` : "\u274C no audits on file";
23
+ return `${authBit} \xB7 ${verifiedBit} \xB7 ${auditBit}`;
24
+ }
17
25
  function renderVerified(p) {
18
26
  const v = p.verifiedBuild;
19
27
  switch (v.state) {
@@ -42,6 +50,8 @@ function renderProgram(p) {
42
50
  "",
43
51
  `\`${p.programId}\`${p.kind ? ` \xB7 kind: \`${p.kind}\`` : ""}`,
44
52
  "",
53
+ `**Trust:** ${renderTrustSummary(p)}`,
54
+ "",
45
55
  `- **Upgrade authority:** ${renderAuthority(p)}`,
46
56
  `- **Verified build:** ${renderVerified(p)}`,
47
57
  `- **Parity:** ${renderParity(p)}`,
@@ -0,0 +1,129 @@
1
+ // src/tokenEconomics.ts
2
+ var ECONOMIC_PATTERNS = [
3
+ {
4
+ id: "metaplex-seller-fee",
5
+ category: "royalty",
6
+ sdk: "Metaplex Token Metadata",
7
+ call: "createV1 / createNft / createFungible",
8
+ field: "sellerFeeBasisPoints",
9
+ whatZeroMeans: "Omitted \u2192 defaults to 0. Creators earn no royalty on any secondary sale, permanently \u2014 the token mints fine and looks correct on-chain.",
10
+ fix: "Set sellerFeeBasisPoints explicitly at mint time (e.g. 500 = 5%). There is no after-the-fact migration once minted.",
11
+ ruleId: "metaplex-seller-fee-zero",
12
+ status: "enforced",
13
+ docsUrl: "https://developers.metaplex.com/token-metadata/mint"
14
+ },
15
+ {
16
+ id: "bags-fee-share-creator",
17
+ category: "fee",
18
+ sdk: "Bags",
19
+ call: "createBagsFeeShareConfig",
20
+ field: "feeClaimers[].userBps (creator inclusion)",
21
+ whatZeroMeans: "The creator wallet omitted from feeClaimers, or the userBps not summing to 10000 \u2192 the creator's share of trading fees is silently zero. This is the original Bags trap.",
22
+ fix: "Include the creator wallet as a feeClaimers entry and ensure every userBps sums to 10000.",
23
+ ruleId: "bags-fee-share-creator-included",
24
+ status: "enforced",
25
+ docsUrl: "https://bags.fm"
26
+ },
27
+ {
28
+ id: "token2022-transfer-fee",
29
+ category: "fee",
30
+ sdk: "SPL Token-2022 (Transfer Fee extension)",
31
+ call: "createInitializeTransferFeeConfigInstruction",
32
+ field: "transferFeeBasisPoints",
33
+ whatZeroMeans: "Initializing the transfer-fee extension with 0 basis points \u2192 no fee is ever withheld on transfers. The extension is 'configured' but collects nothing.",
34
+ fix: "Pass a non-zero transferFeeBasisPoints (and a sensible maximumFee) when initializing the extension.",
35
+ // Positional-argument call — not an object-literal field, so the bundled
36
+ // object-field checker doesn't cover it yet. Advisory until a positional
37
+ // variant ships.
38
+ ruleId: null,
39
+ status: "advisory",
40
+ docsUrl: "https://www.solana-program.com/docs/token-2022/extensions#transfer-fee"
41
+ },
42
+ {
43
+ id: "reward-rate-zero",
44
+ category: "reward",
45
+ sdk: "Staking / LP reward distributors (generic)",
46
+ call: "initialize / configureReward (varies)",
47
+ field: "rewardRate / emissionsPerSecond",
48
+ whatZeroMeans: "A reward-rate or emissions field omitted/zeroed \u2192 stakers and LPs accrue nothing while the pool looks live. A silent, ongoing zero-yield misconfiguration.",
49
+ fix: "Set the reward-rate field explicitly and assert it is non-zero in your deploy script; add a project-local economic-value-zero-or-missing rule for your SDK's call shape.",
50
+ ruleId: null,
51
+ status: "advisory"
52
+ }
53
+ ];
54
+ function getEconomicPattern(id) {
55
+ return ECONOMIC_PATTERNS.find((e) => e.id === id || e.ruleId === id);
56
+ }
57
+ function economicPatternsByCategory(cat) {
58
+ return ECONOMIC_PATTERNS.filter((e) => e.category === cat);
59
+ }
60
+ function enforcedCount(patterns = ECONOMIC_PATTERNS) {
61
+ return patterns.filter((e) => e.status === "enforced").length;
62
+ }
63
+ var CATEGORY_LABEL = {
64
+ royalty: "Royalties",
65
+ fee: "Fees",
66
+ reward: "Reward distribution"
67
+ };
68
+ function renderEconomicsMd(patterns = ECONOMIC_PATTERNS) {
69
+ const L = ["## Token Economics \u2014 silent zero-revenue class\n"];
70
+ L.push(
71
+ "The Bags exploit generalized: a revenue-bearing field that, if omitted or zeroed, silently collects nothing \u2014 forever. Watch these across every protocol that touches fees, royalties, or rewards.\n"
72
+ );
73
+ L.push("| Category | SDK | Field | Status | Rule |");
74
+ L.push("|----------|-----|-------|--------|------|");
75
+ for (const e of patterns) {
76
+ const status = e.status === "enforced" ? "\u2705 enforced" : "\u26A0\uFE0F advisory";
77
+ L.push(`| ${CATEGORY_LABEL[e.category]} | ${e.sdk} | \`${e.field}\` | ${status} | ${e.ruleId ? `\`${e.ruleId}\`` : "\u2014"} |`);
78
+ }
79
+ L.push("");
80
+ L.push(`**${enforcedCount(patterns)} of ${patterns.length} enforced by a bundled rule; the rest are advisories (grep targets / project-local rule candidates).**
81
+ `);
82
+ for (const e of patterns) {
83
+ L.push(`### ${e.sdk} \u2014 \`${e.field}\` (${CATEGORY_LABEL[e.category]})
84
+ `);
85
+ L.push(`- **Call:** \`${e.call}\``);
86
+ L.push(`- **Zero/omitted means:** ${e.whatZeroMeans}`);
87
+ L.push(`- **Fix:** ${e.fix}`);
88
+ L.push(`- **Detection:** ${e.ruleId ? `\`${e.ruleId}\` (bundled)` : "advisory \u2014 no bundled rule yet"}`);
89
+ if (e.docsUrl) L.push(`- **Docs:** ${e.docsUrl}`);
90
+ L.push("");
91
+ }
92
+ return L.join("\n");
93
+ }
94
+ function renderEconomicsText(patterns = ECONOMIC_PATTERNS) {
95
+ const L = [];
96
+ L.push("\u2500\u2500 Token Economics \u2014 silent zero-revenue class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
97
+ L.push(" fields that default to zero and silently collect nothing");
98
+ L.push("");
99
+ for (const e of patterns) {
100
+ const status = e.status === "enforced" ? "[enforced]" : "[advisory]";
101
+ L.push(` ${status} ${CATEGORY_LABEL[e.category]}: ${e.sdk} \xB7 ${e.field}`);
102
+ L.push(` ${e.whatZeroMeans}`);
103
+ if (e.ruleId) L.push(` rule: ${e.ruleId}`);
104
+ L.push("");
105
+ }
106
+ L.push(` ${enforcedCount(patterns)}/${patterns.length} enforced by a bundled rule`);
107
+ return L.join("\n");
108
+ }
109
+ function renderEconomicDetailText(e) {
110
+ const L = [];
111
+ L.push(`\u2500\u2500 ${e.sdk} \u2014 ${e.field} \u2500\u2500`);
112
+ L.push(` category: ${CATEGORY_LABEL[e.category]}`);
113
+ L.push(` call: ${e.call}`);
114
+ L.push(` zero means: ${e.whatZeroMeans}`);
115
+ L.push(` fix: ${e.fix}`);
116
+ L.push(` detection: ${e.ruleId ? `${e.ruleId} (bundled rule)` : "advisory \u2014 no bundled rule yet"}`);
117
+ if (e.docsUrl) L.push(` docs: ${e.docsUrl}`);
118
+ return L.join("\n");
119
+ }
120
+
121
+ export {
122
+ ECONOMIC_PATTERNS,
123
+ getEconomicPattern,
124
+ economicPatternsByCategory,
125
+ enforcedCount,
126
+ renderEconomicsMd,
127
+ renderEconomicsText,
128
+ renderEconomicDetailText
129
+ };
@@ -1050,6 +1050,75 @@ function anchorCpiUnverifiedProgram(c, p) {
1050
1050
  };
1051
1051
  }
1052
1052
 
1053
+ // src/checkers/economicValueZeroOrMissing.ts
1054
+ import { SyntaxKind as SyntaxKind12 } from "ts-morph";
1055
+ function callName6(call) {
1056
+ const exp = call.getExpression();
1057
+ if (exp.getKind() === SyntaxKind12.Identifier) return exp.getText();
1058
+ if (exp.getKind() === SyntaxKind12.PropertyAccessExpression) {
1059
+ return exp.asKind(SyntaxKind12.PropertyAccessExpression).getName();
1060
+ }
1061
+ return "";
1062
+ }
1063
+ function unwrap(expr) {
1064
+ let e = expr;
1065
+ while (e) {
1066
+ const k = e.getKind();
1067
+ if (k === SyntaxKind12.AsExpression || k === SyntaxKind12.ParenthesizedExpression || k === SyntaxKind12.TypeAssertionExpression || k === SyntaxKind12.NonNullExpression) {
1068
+ e = e.getExpression();
1069
+ } else break;
1070
+ }
1071
+ return e;
1072
+ }
1073
+ function isLiteralZero(expr) {
1074
+ const u = unwrap(expr);
1075
+ if (!u) return false;
1076
+ const num = u.asKind(SyntaxKind12.NumericLiteral);
1077
+ if (num) return Number(num.getLiteralValue()) === 0;
1078
+ const big = u.asKind(SyntaxKind12.BigIntLiteral);
1079
+ if (big) return /^0n?$/.test(big.getText().replace(/_/g, ""));
1080
+ return false;
1081
+ }
1082
+ var economicValueZeroOrMissing = (c, p) => {
1083
+ const names = Array.isArray(p.calls) ? p.calls : [p.calls];
1084
+ const field = p.field;
1085
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((x) => names.includes(callName6(x)));
1086
+ if (calls.length === 0) {
1087
+ return {
1088
+ result: "cant_tell",
1089
+ detail: p.absentDetail ?? `No call to ${names.join("/")} in '${c.fnName}'; economic-value check does not apply.`
1090
+ };
1091
+ }
1092
+ for (const call of calls) {
1093
+ const obj = call.getArguments().map((a) => unwrap(a)?.asKind(SyntaxKind12.ObjectLiteralExpression)).find((o) => !!o);
1094
+ if (!obj) continue;
1095
+ const member = obj.getProperty(field);
1096
+ if (!member) {
1097
+ return {
1098
+ result: "fail",
1099
+ detail: p.omittedDetail ?? `'${field}' is omitted from the ${callName6(call)} config \u2014 it defaults to zero. This is a permanent, silent zero-revenue misconfiguration: the call succeeds and no value is ever collected. Set '${field}' explicitly.`
1100
+ };
1101
+ }
1102
+ const pa = member.asKind(SyntaxKind12.PropertyAssignment);
1103
+ const shorthand = member.asKind(SyntaxKind12.ShorthandPropertyAssignment);
1104
+ const init = pa?.getInitializer() ?? shorthand?.getNameNode();
1105
+ if (isLiteralZero(init)) {
1106
+ return {
1107
+ result: "fail",
1108
+ detail: p.zeroDetail ?? `'${field}' is set to a literal 0 in the ${callName6(call)} config \u2014 zero revenue will ever be collected. If intentional, this is a footgun; otherwise set a non-zero value.`
1109
+ };
1110
+ }
1111
+ return {
1112
+ result: "pass",
1113
+ detail: p.passDetail ?? `'${field}' is set to a non-zero value in the ${callName6(call)} config.`
1114
+ };
1115
+ }
1116
+ return {
1117
+ result: "cant_tell",
1118
+ detail: p.absentDetail ?? `Call to ${names.join("/")} found but its config argument isn't an object literal; can't validate '${field}'.`
1119
+ };
1120
+ };
1121
+
1053
1122
  // src/checkers/index.ts
1054
1123
  var registry = {
1055
1124
  "positional-arg-identity": positionalArgIdentity,
@@ -1068,7 +1137,8 @@ var registry = {
1068
1137
  "anchor-account-missing-constraint": anchorAccountMissingConstraint,
1069
1138
  "anchor-forbidden-account-type": anchorForbiddenAccountType,
1070
1139
  "anchor-body-call-pattern": anchorBodyCallPattern,
1071
- "anchor-cpi-unverified-program": anchorCpiUnverifiedProgram
1140
+ "anchor-cpi-unverified-program": anchorCpiUnverifiedProgram,
1141
+ "economic-value-zero-or-missing": economicValueZeroOrMissing
1072
1142
  };
1073
1143
  function runChecker(kind, c, params) {
1074
1144
  const fn = registry[kind];
@@ -1284,7 +1354,7 @@ function findRustCandidates(targetDir, rule) {
1284
1354
  }
1285
1355
 
1286
1356
  // src/fixers/positionalArgIdentity.ts
1287
- import { SyntaxKind as SyntaxKind12 } from "ts-morph";
1357
+ import { SyntaxKind as SyntaxKind13 } from "ts-morph";
1288
1358
 
1289
1359
  // src/fixers/diffUtil.ts
1290
1360
  function buildDiff(node, replacement) {
@@ -1309,9 +1379,9 @@ function buildDiff(node, replacement) {
1309
1379
  // src/fixers/positionalArgIdentity.ts
1310
1380
  var fixPositionalArgIdentity = (c, p, outcome) => {
1311
1381
  if (outcome.result !== "fail") return void 0;
1312
- const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((call) => {
1382
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression).filter((call) => {
1313
1383
  const exp = call.getExpression();
1314
- return exp.getKind() === SyntaxKind12.PropertyAccessExpression && exp.asKind(SyntaxKind12.PropertyAccessExpression).getName() === p.call;
1384
+ return exp.getKind() === SyntaxKind13.PropertyAccessExpression && exp.asKind(SyntaxKind13.PropertyAccessExpression).getName() === p.call;
1315
1385
  });
1316
1386
  if (calls.length === 0) {
1317
1387
  const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
@@ -1326,7 +1396,7 @@ Do not call JSON.parse() on the body before this verification step.`
1326
1396
  }
1327
1397
  const arg = calls[0].getArguments()[p.argIndex];
1328
1398
  const wantParam = c.params[p.paramIndex];
1329
- if (arg && wantParam && arg.getKind() === SyntaxKind12.CallExpression) {
1399
+ if (arg && wantParam && arg.getKind() === SyntaxKind13.CallExpression) {
1330
1400
  return {
1331
1401
  summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
1332
1402
  diff: buildDiff(arg, wantParam)
@@ -1336,12 +1406,12 @@ Do not call JSON.parse() on the body before this verification step.`
1336
1406
  };
1337
1407
 
1338
1408
  // src/fixers/requiredCallWithOptions.ts
1339
- import { SyntaxKind as SyntaxKind13 } from "ts-morph";
1340
- function callName6(call) {
1409
+ import { SyntaxKind as SyntaxKind14 } from "ts-morph";
1410
+ function callName7(call) {
1341
1411
  const exp = call.getExpression();
1342
- if (exp.getKind() === SyntaxKind13.Identifier) return exp.getText();
1343
- if (exp.getKind() === SyntaxKind13.PropertyAccessExpression) {
1344
- return exp.asKind(SyntaxKind13.PropertyAccessExpression).getName();
1412
+ if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
1413
+ if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
1414
+ return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
1345
1415
  }
1346
1416
  return "";
1347
1417
  }
@@ -1359,15 +1429,15 @@ function placeholderFor(propName) {
1359
1429
  }
1360
1430
  var fixRequiredCallWithOptions = (c, p, outcome) => {
1361
1431
  if (outcome.result !== "fail") return void 0;
1362
- const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression);
1363
- const verify = calls.filter((x) => p.verifyCalls.includes(callName6(x)));
1432
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression);
1433
+ const verify = calls.filter((x) => p.verifyCalls.includes(callName7(x)));
1364
1434
  if (verify.length > 0) {
1365
1435
  const call = verify[0];
1366
1436
  const args = call.getArguments();
1367
1437
  const lastArg = args[args.length - 1];
1368
- const obj = lastArg?.asKind(SyntaxKind13.ObjectLiteralExpression);
1438
+ const obj = lastArg?.asKind(SyntaxKind14.ObjectLiteralExpression);
1369
1439
  const presentNames = obj ? obj.getProperties().map((pr) => {
1370
- const pa = pr.asKind(SyntaxKind13.PropertyAssignment) ?? pr.asKind(SyntaxKind13.ShorthandPropertyAssignment);
1440
+ const pa = pr.asKind(SyntaxKind14.PropertyAssignment) ?? pr.asKind(SyntaxKind14.ShorthandPropertyAssignment);
1371
1441
  return pa?.getName() ?? "";
1372
1442
  }) : [];
1373
1443
  const missingGroups = p.requiredProps.filter(
@@ -1375,7 +1445,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
1375
1445
  );
1376
1446
  if (missingGroups.length === 0) return void 0;
1377
1447
  const newProps = missingGroups.map((g) => placeholderFor(g[0])).join(", ");
1378
- const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName6(call)} call`;
1448
+ const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName7(call)} call`;
1379
1449
  if (obj) {
1380
1450
  const inner = obj.getText().slice(1, -1).trim();
1381
1451
  const newText = inner.length > 0 ? `{ ${inner}, ${newProps} }` : `{ ${newProps} }`;
@@ -1387,7 +1457,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
1387
1457
  }
1388
1458
  return {
1389
1459
  summary,
1390
- suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName6(call)}.`
1460
+ suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName7(call)}.`
1391
1461
  };
1392
1462
  }
1393
1463
  return {
@@ -1401,22 +1471,22 @@ JWKS must come from Privy's published JWKS endpoint for your app.`
1401
1471
  };
1402
1472
 
1403
1473
  // src/fixers/literalMultiplierWrongConstant.ts
1404
- import { SyntaxKind as SyntaxKind14 } from "ts-morph";
1405
- function callName7(call) {
1474
+ import { SyntaxKind as SyntaxKind15 } from "ts-morph";
1475
+ function callName8(call) {
1406
1476
  const exp = call.getExpression();
1407
- if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
1408
- if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
1409
- return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
1477
+ if (exp.getKind() === SyntaxKind15.Identifier) return exp.getText();
1478
+ if (exp.getKind() === SyntaxKind15.PropertyAccessExpression) {
1479
+ return exp.asKind(SyntaxKind15.PropertyAccessExpression).getName();
1410
1480
  }
1411
1481
  return "";
1412
1482
  }
1413
1483
  function findIdentifier(node, name) {
1414
- if (node.getKind() === SyntaxKind14.Identifier && node.getText() === name) return node;
1415
- return node.getDescendantsOfKind(SyntaxKind14.Identifier).find((id) => id.getText() === name);
1484
+ if (node.getKind() === SyntaxKind15.Identifier && node.getText() === name) return node;
1485
+ return node.getDescendantsOfKind(SyntaxKind15.Identifier).find((id) => id.getText() === name);
1416
1486
  }
1417
1487
  var fixLiteralMultiplierWrongConstant = (c, p, outcome) => {
1418
1488
  if (outcome.result !== "fail") return void 0;
1419
- const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression).filter((call) => callName7(call) === p.call);
1489
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind15.CallExpression).filter((call) => callName8(call) === p.call);
1420
1490
  if (calls.length === 0) return void 0;
1421
1491
  const arg = calls[0].getArguments()[p.argIndex];
1422
1492
  if (!arg) return void 0;
@@ -0,0 +1,110 @@
1
+ import {
2
+ DEFAULT_RPC
3
+ } from "./chunk-XSVQSK53.js";
4
+ import {
5
+ isValidSolanaAddress
6
+ } from "./chunk-VG5FMOLW.js";
7
+
8
+ // src/oracle.ts
9
+ var SLOT_MS = 400;
10
+ var DEFAULT_STALENESS_SLOTS = 150;
11
+ async function rpc(method, params, opts) {
12
+ const url = opts.rpcUrl ?? DEFAULT_RPC;
13
+ const fetchImpl = opts.fetchImpl ?? fetch;
14
+ const ac = new AbortController();
15
+ const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
16
+ try {
17
+ const res = await fetchImpl(url, {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json" },
20
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
21
+ signal: ac.signal
22
+ });
23
+ if (!res.ok) throw new Error(`rpc ${method}: HTTP ${res.status}`);
24
+ const body = await res.json();
25
+ if (body.error) throw new Error(`rpc ${method}: ${body.error.message}`);
26
+ if (body.result === void 0) throw new Error(`rpc ${method}: empty result`);
27
+ return body.result;
28
+ } finally {
29
+ clearTimeout(t);
30
+ }
31
+ }
32
+ async function checkOracleFreshness(account, opts = {}) {
33
+ if (!isValidSolanaAddress(account)) throw new Error(`invalid Solana address: ${account}`);
34
+ const thresholdSlots = opts.maxStalenessSlots ?? (opts.maxStalenessSeconds != null ? Math.max(1, Math.round(opts.maxStalenessSeconds * 1e3 / SLOT_MS)) : DEFAULT_STALENESS_SLOTS);
35
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
36
+ const currentSlot = await rpc("getSlot", [{ commitment: "confirmed" }], opts);
37
+ const sigs = await rpc(
38
+ "getSignaturesForAddress",
39
+ [account, { limit: 1 }],
40
+ opts
41
+ );
42
+ if (!sigs || sigs.length === 0) {
43
+ return {
44
+ account,
45
+ currentSlot,
46
+ lastSlot: null,
47
+ lastBlockTime: null,
48
+ slotsBehind: null,
49
+ secondsBehind: null,
50
+ thresholdSlots,
51
+ fresh: false,
52
+ verdict: "NO_HISTORY",
53
+ checkedAt
54
+ };
55
+ }
56
+ const last = sigs[0];
57
+ const lastSlot = last.slot;
58
+ const lastBlockTime = last.blockTime ?? null;
59
+ const slotsBehind = Math.max(0, currentSlot - lastSlot);
60
+ const secondsBehind = lastBlockTime != null ? Math.max(0, Math.floor(Date.now() / 1e3) - lastBlockTime) : Math.round(slotsBehind * SLOT_MS / 1e3);
61
+ const fresh = slotsBehind <= thresholdSlots;
62
+ return {
63
+ account,
64
+ currentSlot,
65
+ lastSlot,
66
+ lastBlockTime,
67
+ slotsBehind,
68
+ secondsBehind,
69
+ thresholdSlots,
70
+ fresh,
71
+ verdict: fresh ? "FRESH" : "STALE",
72
+ checkedAt
73
+ };
74
+ }
75
+ function renderOracleText(f) {
76
+ const L = [];
77
+ L.push("\u2500\u2500 Oracle Freshness \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");
78
+ L.push(` account: ${f.account}`);
79
+ L.push(` current slot: ${f.currentSlot.toLocaleString()}`);
80
+ if (f.verdict === "NO_HISTORY") {
81
+ L.push(" last write: (no transactions found touching this account)");
82
+ L.push(" verdict: \u2753 NO_HISTORY \u2014 cannot confirm freshness");
83
+ return L.join("\n");
84
+ }
85
+ L.push(` last write: slot ${f.lastSlot.toLocaleString()} (${f.slotsBehind.toLocaleString()} slots / ~${f.secondsBehind}s ago)`);
86
+ L.push(` threshold: ${f.thresholdSlots.toLocaleString()} slots`);
87
+ L.push(` verdict: ${f.fresh ? "\u2705 FRESH" : "\u{1F6A8} STALE \u2014 last update is older than the freshness threshold"}`);
88
+ return L.join("\n");
89
+ }
90
+ function renderOracleMd(f) {
91
+ const L = ["## Oracle Freshness\n"];
92
+ L.push(`**Account:** \`${f.account}\` \xB7 checked ${f.checkedAt}
93
+ `);
94
+ if (f.verdict === "NO_HISTORY") {
95
+ L.push("\u2753 **NO_HISTORY** \u2014 no transactions were found touching this account, so freshness can't be confirmed. Double-check the address.");
96
+ return L.join("\n");
97
+ }
98
+ const badge = f.fresh ? "\u2705 **FRESH**" : "\u{1F6A8} **STALE**";
99
+ L.push(`${badge} \u2014 last written at slot ${f.lastSlot.toLocaleString()}, ${f.slotsBehind.toLocaleString()} slots (~${f.secondsBehind}s) behind the current slot (${f.currentSlot.toLocaleString()}).`);
100
+ L.push("");
101
+ L.push(`Threshold: ${f.thresholdSlots.toLocaleString()} slots. ${f.fresh ? "Within tolerance." : "**Exceeds tolerance \u2014 do not price against this feed until it updates.**"}`);
102
+ return L.join("\n");
103
+ }
104
+
105
+ export {
106
+ DEFAULT_STALENESS_SLOTS,
107
+ checkOracleFreshness,
108
+ renderOracleText,
109
+ renderOracleMd
110
+ };
package/dist/cli.js CHANGED
@@ -18,21 +18,21 @@ import {
18
18
  submitTelemetry,
19
19
  telemetryFilePath,
20
20
  validatePack
21
- } from "./chunk-5VYTURTO.js";
21
+ } from "./chunk-HFYBK2VA.js";
22
22
  import {
23
23
  renderTrustGraphMd
24
- } from "./chunk-2UZGWXIX.js";
24
+ } from "./chunk-QMJEZ6NO.js";
25
25
  import {
26
26
  buildTrustGraph,
27
27
  cacheSize,
28
28
  defaultCachePath,
29
29
  loadProgramCache
30
- } from "./chunk-SVSVVW6U.js";
30
+ } from "./chunk-GF37ANSW.js";
31
31
  import {
32
32
  audit,
33
33
  getChangedRanges,
34
34
  resolveRules
35
- } from "./chunk-5H5JQXXU.js";
35
+ } from "./chunk-V4XS5DKD.js";
36
36
  import "./chunk-2XJORJPQ.js";
37
37
  import "./chunk-O5Z4ZJHC.js";
38
38
  import "./chunk-XSVQSK53.js";
@@ -480,7 +480,7 @@ if (args[0] === "drift") {
480
480
  process.exit(0);
481
481
  }
482
482
  if (args[0] === "mcp") {
483
- const { startMcpServer } = await import("./mcp-EOTWSQK7.js");
483
+ const { startMcpServer } = await import("./mcp-LITBQHBF.js");
484
484
  await startMcpServer();
485
485
  process.exit(0);
486
486
  }
@@ -563,6 +563,14 @@ if (args[0] === "exploits") {
563
563
  runExploits(args.slice(1));
564
564
  process.exit(0);
565
565
  }
566
+ if (args[0] === "oracle") {
567
+ await runOracle(args.slice(1));
568
+ process.exit(0);
569
+ }
570
+ if (args[0] === "economics") {
571
+ await runEconomics(args.slice(1));
572
+ process.exit(0);
573
+ }
566
574
  if (args[0] === "fix") {
567
575
  await runFix(args.slice(1));
568
576
  process.exit(0);
@@ -700,6 +708,87 @@ function runDeployPlan(argv) {
700
708
  writeFileSync2(mdPath, renderDeployPlanMd(plan));
701
709
  console.log(` deploy plan: ${mdPath}`);
702
710
  }
711
+ async function runEconomics(argv) {
712
+ const {
713
+ ECONOMIC_PATTERNS,
714
+ getEconomicPattern,
715
+ renderEconomicsText,
716
+ renderEconomicsMd,
717
+ renderEconomicDetailText
718
+ } = await import("./tokenEconomics-HBF3DYNH.js");
719
+ if (argv.includes("--help") || argv.includes("-h")) {
720
+ console.log("usage: brainblast economics [id] [--json]");
721
+ console.log(" Token Economics Validator: the silent zero-revenue class (fees, royalties,");
722
+ console.log(" rewards) \u2014 fields that default to zero and quietly collect nothing. Pass an");
723
+ console.log(" id to see one in detail. Known ids:");
724
+ console.log(` ${ECONOMIC_PATTERNS.map((e) => e.id).join(", ")}`);
725
+ process.exit(0);
726
+ }
727
+ const json = argv.includes("--json");
728
+ const id = argv.find((a) => !a.startsWith("--"));
729
+ if (id) {
730
+ const e = getEconomicPattern(id);
731
+ if (!e) {
732
+ console.error(`error: no economic pattern '${id}'. Known: ${ECONOMIC_PATTERNS.map((x) => x.id).join(", ")}`);
733
+ process.exit(2);
734
+ }
735
+ if (json) console.log(JSON.stringify(e, null, 2));
736
+ else console.log(renderEconomicDetailText(e));
737
+ return;
738
+ }
739
+ if (json) {
740
+ console.log(JSON.stringify(ECONOMIC_PATTERNS, null, 2));
741
+ return;
742
+ }
743
+ console.log(renderEconomicsText());
744
+ const outDir2 = join3(process.cwd(), ".agent-research");
745
+ mkdirSync2(outDir2, { recursive: true });
746
+ writeFileSync2(join3(outDir2, "token-economics.md"), renderEconomicsMd());
747
+ console.log(`
748
+ catalog: ${join3(outDir2, "token-economics.md")}`);
749
+ }
750
+ async function runOracle(argv) {
751
+ if (argv.includes("--help") || argv.includes("-h") || argv.filter((a) => !a.startsWith("--")).length === 0) {
752
+ console.log("usage: brainblast oracle <account> [--rpc URL] [--max-staleness-slots N | --max-staleness-seconds N] [--json]");
753
+ console.log(" Is the oracle fresh? Reports how many slots/seconds ago the price account was");
754
+ console.log(" last written (provider-agnostic) and gates FRESH/STALE. Exit 1 on STALE or");
755
+ console.log(" NO_HISTORY. Pass your own --rpc for reliable results (public RPC is rate-limited).");
756
+ process.exit(argv.length === 0 ? 2 : 0);
757
+ }
758
+ const { checkOracleFreshness, renderOracleText, renderOracleMd } = await import("./oracle-DVZLFJ43.js");
759
+ const num = (name) => {
760
+ const i = argv.indexOf(`--${name}`);
761
+ if (i < 0) return void 0;
762
+ const v = parseInt(argv[i + 1], 10);
763
+ return Number.isFinite(v) ? v : void 0;
764
+ };
765
+ const rpcIdx = argv.indexOf("--rpc");
766
+ const rpcUrl = rpcIdx >= 0 ? argv[rpcIdx + 1] : void 0;
767
+ const account = argv.find(
768
+ (a, i) => !a.startsWith("--") && argv[i - 1] !== "--rpc" && argv[i - 1] !== "--max-staleness-slots" && argv[i - 1] !== "--max-staleness-seconds"
769
+ );
770
+ if (!account) {
771
+ console.error("error: missing <account>. usage: brainblast oracle <account> [--rpc URL] [--json]");
772
+ process.exit(2);
773
+ }
774
+ let f;
775
+ try {
776
+ f = await checkOracleFreshness(account, {
777
+ rpcUrl,
778
+ maxStalenessSlots: num("max-staleness-slots"),
779
+ maxStalenessSeconds: num("max-staleness-seconds")
780
+ });
781
+ } catch (e) {
782
+ console.error(`error: ${e?.message ?? String(e)}`);
783
+ process.exit(2);
784
+ }
785
+ if (argv.includes("--json")) console.log(JSON.stringify(f, null, 2));
786
+ else console.log(renderOracleText(f));
787
+ const outDir2 = join3(process.cwd(), ".agent-research");
788
+ mkdirSync2(outDir2, { recursive: true });
789
+ writeFileSync2(join3(outDir2, "oracle-freshness.md"), renderOracleMd(f));
790
+ process.exit(f.fresh ? 0 : 1);
791
+ }
703
792
  function runExploits(argv) {
704
793
  if (argv.includes("--help") || argv.includes("-h")) {
705
794
  console.log("usage: brainblast exploits [id] [--json]");
@@ -1172,8 +1261,8 @@ async function runIdlRules(argv) {
1172
1261
  }
1173
1262
  }
1174
1263
  async function runScore(argv) {
1175
- const { scoreProgram, renderScoreText, gradeAtLeast } = await import("./score-VLKER37D.js");
1176
- const { isValidSolanaAddress: isValidSolanaAddress2 } = await import("./trustGraph-4SSJOQKT.js");
1264
+ const { scoreProgram, renderScoreText, gradeAtLeast } = await import("./score-YXFXD6QG.js");
1265
+ const { isValidSolanaAddress: isValidSolanaAddress2 } = await import("./trustGraph-K5PWNEL4.js");
1177
1266
  const programId = argv.find((a) => !a.startsWith("--"));
1178
1267
  if (!programId) {
1179
1268
  console.error("usage: brainblast score <program-id> [--rpc URL] [--no-probe] [--min A|B|C|D|F] [--json]");
package/dist/index.d.ts CHANGED
@@ -388,6 +388,7 @@ interface UpgradeAuthority {
388
388
  address: string | null;
389
389
  source: UpgradeAuthoritySource;
390
390
  checkedAt?: string;
391
+ ownerProgram?: string;
391
392
  }
392
393
  type VerifiedBuildState = {
393
394
  state: "verified";
@@ -443,6 +444,7 @@ interface RpcOpts {
443
444
 
444
445
  interface BuildOpts extends RpcOpts {
445
446
  probeRpc?: boolean;
447
+ classifyAuthority?: boolean;
446
448
  directoryPath?: string;
447
449
  cachePath?: string | null;
448
450
  }
@@ -452,6 +454,20 @@ declare function renderTrustGraphMd(g: TrustGraph): string;
452
454
 
453
455
  declare function loadDirectory(path?: string): Map<string, OnChainProgram>;
454
456
 
457
+ declare const SYSTEM_PROGRAM = "11111111111111111111111111111111";
458
+ interface KnownOwner {
459
+ kind: Exclude<UpgradeAuthorityKind, "renounced">;
460
+ label: string;
461
+ }
462
+ declare const KNOWN_AUTHORITY_OWNERS: Record<string, KnownOwner>;
463
+ interface AuthorityClassification {
464
+ kind: UpgradeAuthorityKind;
465
+ ownerProgram?: string;
466
+ ownerLabel?: string;
467
+ }
468
+ declare function classifyUpgradeAuthority(address: string, opts?: RpcOpts): Promise<AuthorityClassification>;
469
+ declare function enrichAuthorityClassification(authority: UpgradeAuthority, opts?: RpcOpts): Promise<UpgradeAuthority>;
470
+
455
471
  declare function base58Encode(bytes: Uint8Array): string;
456
472
  declare function base58Decode(s: string): Uint8Array;
457
473
  declare function isValidSolanaAddress(s: string): boolean;
@@ -876,6 +892,62 @@ declare function batchScan(mints: string[], opts?: BatchScanOpts): Promise<Batch
876
892
  declare function parseMintList(content: string): string[];
877
893
  declare function renderBatchText(result: BatchResult): string;
878
894
 
895
+ declare const DEFAULT_STALENESS_SLOTS = 150;
896
+ type OracleVerdict = "FRESH" | "STALE" | "NO_HISTORY";
897
+ interface OracleFreshness {
898
+ account: string;
899
+ currentSlot: number;
900
+ /** Slot of the most recent signature touching the account; null if none. */
901
+ lastSlot: number | null;
902
+ /** Unix seconds of that signature's block, when the RPC provides it. */
903
+ lastBlockTime: number | null;
904
+ slotsBehind: number | null;
905
+ secondsBehind: number | null;
906
+ thresholdSlots: number;
907
+ fresh: boolean;
908
+ verdict: OracleVerdict;
909
+ checkedAt: string;
910
+ }
911
+ interface OracleOpts extends RpcOpts {
912
+ /** Staleness threshold in slots (default 150 ≈ 60s). */
913
+ maxStalenessSlots?: number;
914
+ /** Staleness threshold in seconds; converted to slots (~400ms each). */
915
+ maxStalenessSeconds?: number;
916
+ }
917
+ declare function checkOracleFreshness(account: string, opts?: OracleOpts): Promise<OracleFreshness>;
918
+ declare function renderOracleText(f: OracleFreshness): string;
919
+ declare function renderOracleMd(f: OracleFreshness): string;
920
+
921
+ type EconomicCategory = "royalty" | "fee" | "reward";
922
+ type EconomicStatus = "enforced" | "advisory";
923
+ interface EconomicPattern {
924
+ /** Stable slug. */
925
+ id: string;
926
+ category: EconomicCategory;
927
+ /** SDK / protocol the field belongs to. */
928
+ sdk: string;
929
+ /** The setup/config call that takes the field. */
930
+ call: string;
931
+ /** The revenue-bearing field that silently defaults to zero. */
932
+ field: string;
933
+ /** What a zero/omitted value costs you, in one sentence. */
934
+ whatZeroMeans: string;
935
+ /** The safe pattern. */
936
+ fix: string;
937
+ /** Bundled rule that detects this, or null for an advisory entry. */
938
+ ruleId: string | null;
939
+ status: EconomicStatus;
940
+ /** Optional reference / docs URL. */
941
+ docsUrl?: string;
942
+ }
943
+ declare const ECONOMIC_PATTERNS: EconomicPattern[];
944
+ declare function getEconomicPattern(id: string): EconomicPattern | undefined;
945
+ declare function economicPatternsByCategory(cat: EconomicCategory): EconomicPattern[];
946
+ declare function enforcedCount(patterns?: EconomicPattern[]): number;
947
+ declare function renderEconomicsMd(patterns?: EconomicPattern[]): string;
948
+ declare function renderEconomicsText(patterns?: EconomicPattern[]): string;
949
+ declare function renderEconomicDetailText(e: EconomicPattern): string;
950
+
879
951
  interface ExploitPattern {
880
952
  /** Stable slug, e.g. "wormhole". */
881
953
  id: string;
@@ -905,4 +977,4 @@ declare function renderExploitsMd(patterns?: ExploitPattern[]): string;
905
977
  declare function renderExploitsText(patterns?: ExploitPattern[]): string;
906
978
  declare function renderExploitDetailText(e: ExploitPattern): string;
907
979
 
908
- export { type AccountFlow, type AnchorIdl, type AuditRef, type BatchResult, type BatchRow, type BatchScanOpts, type BuildOpts, CANONICAL_BY_MINT, CANONICAL_MINTS, type Candidate, type CanonicalMint, type ChainEvent, type ChainWatchOpts, type ChainWatchState, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_TTL_HOURS, type DecodedInstruction, type DecodedTx, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, EXPLOIT_PATTERNS, type ExploitPattern, type FirewallFinding, type FirewallOpts, type FirewallProgram, type FirewallReport, type FirewallSeverity, type FirewallVerdict, type Grade, type GraduationEvent, type IdentityStatus, type IdlAccount, type IdlConstraintParams, type IdlInstruction, KNOWN_PROGRAMS, type MintInfo, type OnChainProgram, type OsvAdvisory, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PreflightCheck, type PreflightOpts, type PreflightReport, type PreflightStatus, type PreflightVerdict, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type RicoOutcome, type RicoResult, type RicoTokenSecurity, type Rule, type RustAccountField, type RustCandidate, type RustChecker, type ScoreFactor, type Severity, type TelemetrySubmitResult, type TokenIdentity, type TrustGraph, type TrustScore, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type VerifyOpts, type WatchEvent, type WatchOptions, analyzeCosts, analyzeInstructions, analyzeToken, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, batchScan, buildConstraintParams, buildTrustGraph, rules as bundledRules, cacheSize, canonicalMintForSymbol, checkDrift, checkerKinds, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, fileChanged, findCandidates, findConfigCandidates, formatUsd, generateRulesFromIdl, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getExploitPattern, getRepoHash, getUserHash, getWorkingTreeChanges, gradeAtLeast, gradeForScore, idlProgramName, initPack, initialChainWatchState, inspectTransaction, isCanonicalMint, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseCpiPrograms, parseDiff, parseIdl, parseMintAccount, parseMintList, pollChainOnce, pumpPreflight, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderBatchText, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderExploitDetailText, renderExploitsMd, renderExploitsText, renderFirewallText, renderPreflightText, renderRicoText, renderRulesYaml, renderScoreText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, scoreFromProgram, scoreProgram, seedPackages, startChainWatch, startWatch, submitTelemetry, telemetryFilePath, testKinds, toSnakeCase, totalLossUsd, validatePack, validatePackManifest, verifyTokenIdentity };
980
+ export { type AccountFlow, type AnchorIdl, type AuditRef, type AuthorityClassification, type BatchResult, type BatchRow, type BatchScanOpts, type BuildOpts, CANONICAL_BY_MINT, CANONICAL_MINTS, type Candidate, type CanonicalMint, type ChainEvent, type ChainWatchOpts, type ChainWatchState, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_STALENESS_SLOTS, DEFAULT_TTL_HOURS, type DecodedInstruction, type DecodedTx, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, ECONOMIC_PATTERNS, EXPLOIT_PATTERNS, type EconomicCategory, type EconomicPattern, type EconomicStatus, type ExploitPattern, type FirewallFinding, type FirewallOpts, type FirewallProgram, type FirewallReport, type FirewallSeverity, type FirewallVerdict, type Grade, type GraduationEvent, type IdentityStatus, type IdlAccount, type IdlConstraintParams, type IdlInstruction, KNOWN_AUTHORITY_OWNERS, KNOWN_PROGRAMS, type MintInfo, type OnChainProgram, type OracleFreshness, type OracleOpts, type OracleVerdict, type OsvAdvisory, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PreflightCheck, type PreflightOpts, type PreflightReport, type PreflightStatus, type PreflightVerdict, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type RicoOutcome, type RicoResult, type RicoTokenSecurity, type Rule, type RustAccountField, type RustCandidate, type RustChecker, SYSTEM_PROGRAM, type ScoreFactor, type Severity, type TelemetrySubmitResult, type TokenIdentity, type TrustGraph, type TrustScore, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type VerifyOpts, type WatchEvent, type WatchOptions, analyzeCosts, analyzeInstructions, analyzeToken, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, batchScan, buildConstraintParams, buildTrustGraph, rules as bundledRules, cacheSize, canonicalMintForSymbol, checkDrift, checkOracleFreshness, checkerKinds, classifyUpgradeAuthority, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, economicPatternsByCategory, enforcedCount, enrichAuthorityClassification, fileChanged, findCandidates, findConfigCandidates, formatUsd, generateRulesFromIdl, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getEconomicPattern, getExploitPattern, getRepoHash, getUserHash, getWorkingTreeChanges, gradeAtLeast, gradeForScore, idlProgramName, initPack, initialChainWatchState, inspectTransaction, isCanonicalMint, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseCpiPrograms, parseDiff, parseIdl, parseMintAccount, parseMintList, pollChainOnce, pumpPreflight, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderBatchText, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderEconomicDetailText, renderEconomicsMd, renderEconomicsText, renderExploitDetailText, renderExploitsMd, renderExploitsText, renderFirewallText, renderOracleMd, renderOracleText, renderPreflightText, renderRicoText, renderRulesYaml, renderScoreText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, scoreFromProgram, scoreProgram, seedPackages, startChainWatch, startWatch, submitTelemetry, telemetryFilePath, testKinds, toSnakeCase, totalLossUsd, validatePack, validatePackManifest, verifyTokenIdentity };
package/dist/index.js CHANGED
@@ -1,3 +1,23 @@
1
+ import {
2
+ batchScan,
3
+ parseMintList,
4
+ renderBatchText
5
+ } from "./chunk-QC27GNQ7.js";
6
+ import {
7
+ ECONOMIC_PATTERNS,
8
+ economicPatternsByCategory,
9
+ enforcedCount,
10
+ getEconomicPattern,
11
+ renderEconomicDetailText,
12
+ renderEconomicsMd,
13
+ renderEconomicsText
14
+ } from "./chunk-S7X53LWF.js";
15
+ import {
16
+ DEFAULT_STALENESS_SLOTS,
17
+ checkOracleFreshness,
18
+ renderOracleMd,
19
+ renderOracleText
20
+ } from "./chunk-WQFLKWBY.js";
1
21
  import {
2
22
  checkDrift,
3
23
  renderDriftText,
@@ -17,17 +37,12 @@ import {
17
37
  renderScoreText,
18
38
  scoreFromProgram,
19
39
  scoreProgram
20
- } from "./chunk-UWE6HAGS.js";
40
+ } from "./chunk-F7QOW3IM.js";
21
41
  import {
22
42
  parseMintAccount,
23
43
  pumpPreflight,
24
44
  renderPreflightText
25
45
  } from "./chunk-WX3IR7LK.js";
26
- import {
27
- batchScan,
28
- parseMintList,
29
- renderBatchText
30
- } from "./chunk-QC27GNQ7.js";
31
46
  import {
32
47
  analyzeToken,
33
48
  deployerFlagsFrom,
@@ -61,15 +76,19 @@ import {
61
76
  telemetryFilePath,
62
77
  totalLossUsd,
63
78
  validatePack
64
- } from "./chunk-5VYTURTO.js";
79
+ } from "./chunk-HFYBK2VA.js";
65
80
  import {
66
81
  renderTrustGraphMd
67
- } from "./chunk-2UZGWXIX.js";
82
+ } from "./chunk-QMJEZ6NO.js";
68
83
  import {
69
84
  DEFAULT_TTL_HOURS,
85
+ KNOWN_AUTHORITY_OWNERS,
86
+ SYSTEM_PROGRAM,
70
87
  buildTrustGraph,
71
88
  cacheSize,
89
+ classifyUpgradeAuthority,
72
90
  defaultCachePath,
91
+ enrichAuthorityClassification,
73
92
  getCacheEntry,
74
93
  getCacheEntryMeta,
75
94
  isEntryExpired,
@@ -77,7 +96,7 @@ import {
77
96
  loadProgramCache,
78
97
  putCacheEntry,
79
98
  saveProgramCache
80
- } from "./chunk-SVSVVW6U.js";
99
+ } from "./chunk-GF37ANSW.js";
81
100
  import {
82
101
  PACK_MANIFEST_FILE,
83
102
  audit,
@@ -98,7 +117,7 @@ import {
98
117
  runChecker,
99
118
  testKinds,
100
119
  validatePackManifest
101
- } from "./chunk-5H5JQXXU.js";
120
+ } from "./chunk-V4XS5DKD.js";
102
121
  import {
103
122
  CANONICAL_BY_MINT,
104
123
  CANONICAL_MINTS,
@@ -150,10 +169,14 @@ export {
150
169
  CANONICAL_BY_MINT,
151
170
  CANONICAL_MINTS,
152
171
  DEFAULT_REGISTRY_URL,
172
+ DEFAULT_STALENESS_SLOTS,
153
173
  DEFAULT_TTL_HOURS,
174
+ ECONOMIC_PATTERNS,
154
175
  EXPLOIT_PATTERNS,
176
+ KNOWN_AUTHORITY_OWNERS,
155
177
  KNOWN_PROGRAMS,
156
178
  PACK_MANIFEST_FILE,
179
+ SYSTEM_PROGRAM,
157
180
  analyzeCosts,
158
181
  analyzeInstructions,
159
182
  analyzeToken,
@@ -169,11 +192,16 @@ export {
169
192
  cacheSize,
170
193
  canonicalMintForSymbol,
171
194
  checkDrift,
195
+ checkOracleFreshness,
172
196
  checkerKinds,
197
+ classifyUpgradeAuthority,
173
198
  decodeTransaction,
174
199
  defaultCachePath,
175
200
  deployerFlagsFrom,
176
201
  diffVersions,
202
+ economicPatternsByCategory,
203
+ enforcedCount,
204
+ enrichAuthorityClassification,
177
205
  fileChanged,
178
206
  findCandidates,
179
207
  findConfigCandidates,
@@ -183,6 +211,7 @@ export {
183
211
  getCacheEntry,
184
212
  getCacheEntryMeta,
185
213
  getChangedRanges,
214
+ getEconomicPattern,
186
215
  getExploitPattern,
187
216
  getRepoHash,
188
217
  getUserHash,
@@ -219,10 +248,15 @@ export {
219
248
  renderDiffMd,
220
249
  renderDiffText,
221
250
  renderDriftText,
251
+ renderEconomicDetailText,
252
+ renderEconomicsMd,
253
+ renderEconomicsText,
222
254
  renderExploitDetailText,
223
255
  renderExploitsMd,
224
256
  renderExploitsText,
225
257
  renderFirewallText,
258
+ renderOracleMd,
259
+ renderOracleText,
226
260
  renderPreflightText,
227
261
  renderRicoText,
228
262
  renderRulesYaml,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  audit,
3
3
  resolveRules
4
- } from "./chunk-5H5JQXXU.js";
4
+ } from "./chunk-V4XS5DKD.js";
5
5
  import "./chunk-2XJORJPQ.js";
6
6
  import "./chunk-O5Z4ZJHC.js";
7
7
  import {
@@ -0,0 +1,15 @@
1
+ import {
2
+ DEFAULT_STALENESS_SLOTS,
3
+ checkOracleFreshness,
4
+ renderOracleMd,
5
+ renderOracleText
6
+ } from "./chunk-WQFLKWBY.js";
7
+ import "./chunk-XSVQSK53.js";
8
+ import "./chunk-VG5FMOLW.js";
9
+ import "./chunk-3RG5ZIWI.js";
10
+ export {
11
+ DEFAULT_STALENESS_SLOTS,
12
+ checkOracleFreshness,
13
+ renderOracleMd,
14
+ renderOracleText
15
+ };
@@ -0,0 +1,41 @@
1
+ # Token Economics Validator (v0.7.5) — the generalized Bags exploit.
2
+ # A revenue-bearing field omitted from a config object silently defaults to
3
+ # zero: the call succeeds, nothing is collected, forever. Here: Metaplex
4
+ # `sellerFeeBasisPoints` — the on-chain royalty creators earn on secondary
5
+ # sales. Omit it (or set 0) and the creators earn nothing, permanently.
6
+ id: metaplex-seller-fee-zero
7
+ severity: high
8
+ title: Metaplex token created with sellerFeeBasisPoints omitted or zero — creators earn no royalties
9
+ component:
10
+ name: Metaplex Token Metadata
11
+ type: Blockchain
12
+ version: unversioned
13
+ sourceUrl: https://developers.metaplex.com/token-metadata/mint
14
+ detect:
15
+ modules:
16
+ - "@metaplex-foundation/mpl-token-metadata"
17
+ - "@metaplex-foundation/js"
18
+ nameRegex: "metaplex|mpl|createNft|mint"
19
+ triggerCalls: [create, createV1, createNft, createFungible, createAndMint]
20
+ check:
21
+ kind: economic-value-zero-or-missing
22
+ params:
23
+ calls: [create, createV1, createNft, createFungible, createAndMint]
24
+ field: sellerFeeBasisPoints
25
+ omittedDetail: >-
26
+ The Metaplex create call omits sellerFeeBasisPoints, which defaults to 0.
27
+ The token mints fine, but creators earn ZERO royalties on every secondary
28
+ sale — permanently and silently. There is no after-the-fact fix once
29
+ minted. Set sellerFeeBasisPoints explicitly (e.g. 500 = 5%).
30
+ zeroDetail: >-
31
+ sellerFeeBasisPoints is set to a literal 0 — creators will earn no royalty
32
+ on secondary sales. If this token is meant to be royalty-free that's fine;
33
+ otherwise set a non-zero basis-points value.
34
+ passDetail: >-
35
+ sellerFeeBasisPoints is set to a non-zero value — secondary-sale royalties
36
+ are configured.
37
+ absentDetail: >-
38
+ Handler is in Metaplex scope but does not call a token-create function;
39
+ the royalty-allocation rule does not apply here.
40
+ test:
41
+ kind: none
@@ -4,8 +4,8 @@ import {
4
4
  renderScoreText,
5
5
  scoreFromProgram,
6
6
  scoreProgram
7
- } from "./chunk-UWE6HAGS.js";
8
- import "./chunk-SVSVVW6U.js";
7
+ } from "./chunk-F7QOW3IM.js";
8
+ import "./chunk-GF37ANSW.js";
9
9
  import "./chunk-XSVQSK53.js";
10
10
  import "./chunk-VG5FMOLW.js";
11
11
  import "./chunk-3RG5ZIWI.js";
@@ -0,0 +1,19 @@
1
+ import {
2
+ ECONOMIC_PATTERNS,
3
+ economicPatternsByCategory,
4
+ enforcedCount,
5
+ getEconomicPattern,
6
+ renderEconomicDetailText,
7
+ renderEconomicsMd,
8
+ renderEconomicsText
9
+ } from "./chunk-S7X53LWF.js";
10
+ import "./chunk-3RG5ZIWI.js";
11
+ export {
12
+ ECONOMIC_PATTERNS,
13
+ economicPatternsByCategory,
14
+ enforcedCount,
15
+ getEconomicPattern,
16
+ renderEconomicDetailText,
17
+ renderEconomicsMd,
18
+ renderEconomicsText
19
+ };
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  renderProgram,
3
3
  renderTrustGraphMd
4
- } from "./chunk-2UZGWXIX.js";
4
+ } from "./chunk-QMJEZ6NO.js";
5
5
  import {
6
6
  DEFAULT_TTL_HOURS,
7
+ KNOWN_AUTHORITY_OWNERS,
8
+ SYSTEM_PROGRAM,
7
9
  buildTrustGraph,
8
10
  cacheSize,
11
+ classifyUpgradeAuthority,
9
12
  defaultCachePath,
13
+ enrichAuthorityClassification,
10
14
  getCacheEntry,
11
15
  getCacheEntryMeta,
12
16
  isEntryExpired,
@@ -14,7 +18,7 @@ import {
14
18
  loadProgramCache,
15
19
  putCacheEntry,
16
20
  saveProgramCache
17
- } from "./chunk-SVSVVW6U.js";
21
+ } from "./chunk-GF37ANSW.js";
18
22
  import {
19
23
  DEFAULT_RPC,
20
24
  getAccountInfo,
@@ -29,11 +33,15 @@ import "./chunk-3RG5ZIWI.js";
29
33
  export {
30
34
  DEFAULT_RPC,
31
35
  DEFAULT_TTL_HOURS,
36
+ KNOWN_AUTHORITY_OWNERS,
37
+ SYSTEM_PROGRAM,
32
38
  base58Decode,
33
39
  base58Encode,
34
40
  buildTrustGraph,
35
41
  cacheSize,
42
+ classifyUpgradeAuthority,
36
43
  defaultCachePath,
44
+ enrichAuthorityClassification,
37
45
  getAccountInfo,
38
46
  getCacheEntry,
39
47
  getCacheEntryMeta,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "type": "module",
5
5
  "description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
6
6
  "keywords": [