brainblast 0.7.4 → 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.
@@ -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";
@@ -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;
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ 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
24
  } from "./chunk-QMJEZ6NO.js";
@@ -32,7 +32,7 @@ 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
  }
@@ -567,6 +567,10 @@ if (args[0] === "oracle") {
567
567
  await runOracle(args.slice(1));
568
568
  process.exit(0);
569
569
  }
570
+ if (args[0] === "economics") {
571
+ await runEconomics(args.slice(1));
572
+ process.exit(0);
573
+ }
570
574
  if (args[0] === "fix") {
571
575
  await runFix(args.slice(1));
572
576
  process.exit(0);
@@ -704,6 +708,45 @@ function runDeployPlan(argv) {
704
708
  writeFileSync2(mdPath, renderDeployPlanMd(plan));
705
709
  console.log(` deploy plan: ${mdPath}`);
706
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
+ }
707
750
  async function runOracle(argv) {
708
751
  if (argv.includes("--help") || argv.includes("-h") || argv.filter((a) => !a.startsWith("--")).length === 0) {
709
752
  console.log("usage: brainblast oracle <account> [--rpc URL] [--max-staleness-slots N | --max-staleness-seconds N] [--json]");
package/dist/index.d.ts CHANGED
@@ -918,6 +918,36 @@ declare function checkOracleFreshness(account: string, opts?: OracleOpts): Promi
918
918
  declare function renderOracleText(f: OracleFreshness): string;
919
919
  declare function renderOracleMd(f: OracleFreshness): string;
920
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
+
921
951
  interface ExploitPattern {
922
952
  /** Stable slug, e.g. "wormhole". */
923
953
  id: string;
@@ -947,4 +977,4 @@ declare function renderExploitsMd(patterns?: ExploitPattern[]): string;
947
977
  declare function renderExploitsText(patterns?: ExploitPattern[]): string;
948
978
  declare function renderExploitDetailText(e: ExploitPattern): string;
949
979
 
950
- 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, 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_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, enrichAuthorityClassification, 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, 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 };
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,17 @@
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";
1
15
  import {
2
16
  DEFAULT_STALENESS_SLOTS,
3
17
  checkOracleFreshness,
@@ -29,11 +43,6 @@ import {
29
43
  pumpPreflight,
30
44
  renderPreflightText
31
45
  } from "./chunk-WX3IR7LK.js";
32
- import {
33
- batchScan,
34
- parseMintList,
35
- renderBatchText
36
- } from "./chunk-QC27GNQ7.js";
37
46
  import {
38
47
  analyzeToken,
39
48
  deployerFlagsFrom,
@@ -67,7 +76,7 @@ import {
67
76
  telemetryFilePath,
68
77
  totalLossUsd,
69
78
  validatePack
70
- } from "./chunk-5VYTURTO.js";
79
+ } from "./chunk-HFYBK2VA.js";
71
80
  import {
72
81
  renderTrustGraphMd
73
82
  } from "./chunk-QMJEZ6NO.js";
@@ -108,7 +117,7 @@ import {
108
117
  runChecker,
109
118
  testKinds,
110
119
  validatePackManifest
111
- } from "./chunk-5H5JQXXU.js";
120
+ } from "./chunk-V4XS5DKD.js";
112
121
  import {
113
122
  CANONICAL_BY_MINT,
114
123
  CANONICAL_MINTS,
@@ -162,6 +171,7 @@ export {
162
171
  DEFAULT_REGISTRY_URL,
163
172
  DEFAULT_STALENESS_SLOTS,
164
173
  DEFAULT_TTL_HOURS,
174
+ ECONOMIC_PATTERNS,
165
175
  EXPLOIT_PATTERNS,
166
176
  KNOWN_AUTHORITY_OWNERS,
167
177
  KNOWN_PROGRAMS,
@@ -189,6 +199,8 @@ export {
189
199
  defaultCachePath,
190
200
  deployerFlagsFrom,
191
201
  diffVersions,
202
+ economicPatternsByCategory,
203
+ enforcedCount,
192
204
  enrichAuthorityClassification,
193
205
  fileChanged,
194
206
  findCandidates,
@@ -199,6 +211,7 @@ export {
199
211
  getCacheEntry,
200
212
  getCacheEntryMeta,
201
213
  getChangedRanges,
214
+ getEconomicPattern,
202
215
  getExploitPattern,
203
216
  getRepoHash,
204
217
  getUserHash,
@@ -235,6 +248,9 @@ export {
235
248
  renderDiffMd,
236
249
  renderDiffText,
237
250
  renderDriftText,
251
+ renderEconomicDetailText,
252
+ renderEconomicsMd,
253
+ renderEconomicsText,
238
254
  renderExploitDetailText,
239
255
  renderExploitsMd,
240
256
  renderExploitsText,
@@ -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,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
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.7.4",
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": [