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.
- package/dist/{chunk-UWE6HAGS.js → chunk-F7QOW3IM.js} +1 -1
- package/dist/{chunk-SVSVVW6U.js → chunk-GF37ANSW.js} +46 -0
- package/dist/{chunk-5VYTURTO.js → chunk-HFYBK2VA.js} +1 -1
- package/dist/{chunk-2UZGWXIX.js → chunk-QMJEZ6NO.js} +14 -4
- package/dist/chunk-S7X53LWF.js +129 -0
- package/dist/{chunk-5H5JQXXU.js → chunk-V4XS5DKD.js} +94 -24
- package/dist/chunk-WQFLKWBY.js +110 -0
- package/dist/cli.js +96 -7
- package/dist/index.d.ts +73 -1
- package/dist/index.js +44 -10
- package/dist/{mcp-EOTWSQK7.js → mcp-LITBQHBF.js} +1 -1
- package/dist/oracle-DVZLFJ43.js +15 -0
- package/dist/rules/metaplex-seller-fee-zero.yaml +41 -0
- package/dist/{score-VLKER37D.js → score-YXFXD6QG.js} +2 -2
- package/dist/tokenEconomics-HBF3DYNH.js +19 -0
- package/dist/{trustGraph-4SSJOQKT.js → trustGraph-K5PWNEL4.js} +10 -2
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -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}
|
|
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
|
|
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(
|
|
1382
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression).filter((call) => {
|
|
1313
1383
|
const exp = call.getExpression();
|
|
1314
|
-
return exp.getKind() ===
|
|
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() ===
|
|
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
|
|
1340
|
-
function
|
|
1409
|
+
import { SyntaxKind as SyntaxKind14 } from "ts-morph";
|
|
1410
|
+
function callName7(call) {
|
|
1341
1411
|
const exp = call.getExpression();
|
|
1342
|
-
if (exp.getKind() ===
|
|
1343
|
-
if (exp.getKind() ===
|
|
1344
|
-
return exp.asKind(
|
|
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(
|
|
1363
|
-
const verify = calls.filter((x) => p.verifyCalls.includes(
|
|
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(
|
|
1438
|
+
const obj = lastArg?.asKind(SyntaxKind14.ObjectLiteralExpression);
|
|
1369
1439
|
const presentNames = obj ? obj.getProperties().map((pr) => {
|
|
1370
|
-
const pa = pr.asKind(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
1405
|
-
function
|
|
1474
|
+
import { SyntaxKind as SyntaxKind15 } from "ts-morph";
|
|
1475
|
+
function callName8(call) {
|
|
1406
1476
|
const exp = call.getExpression();
|
|
1407
|
-
if (exp.getKind() ===
|
|
1408
|
-
if (exp.getKind() ===
|
|
1409
|
-
return exp.asKind(
|
|
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() ===
|
|
1415
|
-
return node.getDescendantsOfKind(
|
|
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(
|
|
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-
|
|
21
|
+
} from "./chunk-HFYBK2VA.js";
|
|
22
22
|
import {
|
|
23
23
|
renderTrustGraphMd
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-QMJEZ6NO.js";
|
|
25
25
|
import {
|
|
26
26
|
buildTrustGraph,
|
|
27
27
|
cacheSize,
|
|
28
28
|
defaultCachePath,
|
|
29
29
|
loadProgramCache
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-GF37ANSW.js";
|
|
31
31
|
import {
|
|
32
32
|
audit,
|
|
33
33
|
getChangedRanges,
|
|
34
34
|
resolveRules
|
|
35
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
1176
|
-
const { isValidSolanaAddress: isValidSolanaAddress2 } = await import("./trustGraph-
|
|
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-
|
|
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-
|
|
79
|
+
} from "./chunk-HFYBK2VA.js";
|
|
65
80
|
import {
|
|
66
81
|
renderTrustGraphMd
|
|
67
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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,
|
|
@@ -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-
|
|
8
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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
|
+
"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": [
|