brainblast 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/batchScan-JR2G5JCF.js +14 -0
- package/dist/chunk-2UZGWXIX.js +77 -0
- package/dist/{chunk-HXQNNGSC.js → chunk-34VXOLJF.js} +32 -433
- package/dist/{chunk-2Y6UILTZ.js → chunk-CRYFCQYM.js} +63 -1
- package/dist/chunk-DQ4KAYKQ.js +111 -0
- package/dist/chunk-HL7NVANZ.js +331 -0
- package/dist/chunk-O5Z4ZJHC.js +89 -0
- package/dist/chunk-QC27GNQ7.js +101 -0
- package/dist/chunk-SVSVVW6U.js +187 -0
- package/dist/chunk-UWE6HAGS.js +176 -0
- package/dist/chunk-VG5FMOLW.js +61 -0
- package/dist/chunk-WX3IR7LK.js +148 -0
- package/dist/chunk-XSVQSK53.js +100 -0
- package/dist/cli.js +238 -9
- package/dist/firewall-HN5XJLGC.js +18 -0
- package/dist/idlRules-3KZML4NL.js +17 -0
- package/dist/index.d.ts +238 -1
- package/dist/index.js +105 -34
- package/dist/{mcp-FCKMS2MQ.js → mcp-ML2X44WE.js} +3 -2
- package/dist/pumpCheck-K2ESOBNU.js +16 -0
- package/dist/rpc-W5F4KXS2.js +18 -0
- package/dist/score-VLKER37D.js +18 -0
- package/dist/trustGraph-4SSJOQKT.js +49 -0
- package/dist/watchChain-F6INXAPA.js +13 -0
- package/package.json +2 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CANONICAL_MINTS
|
|
3
3
|
} from "./chunk-2XJORJPQ.js";
|
|
4
|
+
import {
|
|
5
|
+
toSnakeCase
|
|
6
|
+
} from "./chunk-O5Z4ZJHC.js";
|
|
4
7
|
|
|
5
8
|
// src/finder.ts
|
|
6
9
|
import { Project, SyntaxKind } from "ts-morph";
|
|
@@ -852,6 +855,64 @@ var solanaMintIdentity = (c, _params) => {
|
|
|
852
855
|
return { result: "cant_tell", detail: "No symbol-named mint constants found" };
|
|
853
856
|
};
|
|
854
857
|
|
|
858
|
+
// src/checkers/anchorIdlAccount.ts
|
|
859
|
+
function fieldIsSigner(f) {
|
|
860
|
+
if (/\bSigner\s*</.test(f.typeName)) return true;
|
|
861
|
+
if (/\bsigner\b/.test(f.attrText)) return true;
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
function fieldIsMut(f) {
|
|
865
|
+
if (/\bmut\b/.test(f.attrText)) return true;
|
|
866
|
+
if (/\binit\b/.test(f.attrText)) return true;
|
|
867
|
+
if (f.hasInitIfNeeded) return true;
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
function anchorIdlAccount(c, params) {
|
|
871
|
+
const handlerName = toSnakeCase(c.fnName);
|
|
872
|
+
const spec = params?.instructions?.find((i) => i.name === handlerName);
|
|
873
|
+
if (!spec) {
|
|
874
|
+
return {
|
|
875
|
+
result: "cant_tell",
|
|
876
|
+
detail: `Handler '${c.fnName}' is not declared in the ${params?.idlName ?? "IDL"}; constraint rule does not apply.`
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (spec.signers.length === 0 && spec.mutable.length === 0) {
|
|
880
|
+
return {
|
|
881
|
+
result: "cant_tell",
|
|
882
|
+
detail: `Instruction '${spec.name}' declares no signer or mutable accounts in the IDL; nothing to verify.`
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const byName = /* @__PURE__ */ new Map();
|
|
886
|
+
for (const f of c.accountFields) byName.set(toSnakeCase(f.name), f);
|
|
887
|
+
const violations = [];
|
|
888
|
+
for (const acct of spec.signers) {
|
|
889
|
+
const f = byName.get(acct);
|
|
890
|
+
if (!f) {
|
|
891
|
+
violations.push(`'${acct}' (IDL signer) is not present in the Accounts struct`);
|
|
892
|
+
} else if (!fieldIsSigner(f)) {
|
|
893
|
+
violations.push(`'${acct}' must be a Signer (IDL marks it isSigner) but the Rust field has no signer constraint`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
for (const acct of spec.mutable) {
|
|
897
|
+
const f = byName.get(acct);
|
|
898
|
+
if (!f) {
|
|
899
|
+
violations.push(`'${acct}' (IDL mutable) is not present in the Accounts struct`);
|
|
900
|
+
} else if (!fieldIsMut(f)) {
|
|
901
|
+
violations.push(`'${acct}' must be mutable (IDL marks it isMut) but the Rust field has no mut/init constraint`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (violations.length > 0) {
|
|
905
|
+
return {
|
|
906
|
+
result: "fail",
|
|
907
|
+
detail: `Handler '${c.fnName}' diverges from the ${params.idlName} IDL: ` + violations.join("; ") + ". A missing signer/mut constraint is a silent authorization hole."
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
result: "pass",
|
|
912
|
+
detail: `Handler '${c.fnName}' declares all ${spec.signers.length} signer and ${spec.mutable.length} mutable account(s) the ${params.idlName} IDL requires.`
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
855
916
|
// src/checkers/index.ts
|
|
856
917
|
var registry = {
|
|
857
918
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -865,7 +926,8 @@ var registry = {
|
|
|
865
926
|
"taint-to-sink": taintToSink,
|
|
866
927
|
"literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
|
|
867
928
|
"forbidden-call-replacement": forbiddenCallReplacement,
|
|
868
|
-
"solana-mint-identity-mismatch": solanaMintIdentity
|
|
929
|
+
"solana-mint-identity-mismatch": solanaMintIdentity,
|
|
930
|
+
"anchor-account-matches-idl": anchorIdlAccount
|
|
869
931
|
};
|
|
870
932
|
function runChecker(kind, c, params) {
|
|
871
933
|
const fn = registry[kind];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RPC,
|
|
3
|
+
probeUpgradeAuthority
|
|
4
|
+
} from "./chunk-XSVQSK53.js";
|
|
5
|
+
|
|
6
|
+
// src/watchChain.ts
|
|
7
|
+
async function getSignaturesForAddress(programId, until, opts) {
|
|
8
|
+
const url = opts.rpcUrl ?? DEFAULT_RPC;
|
|
9
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
10
|
+
const params = [programId, { limit: opts.limit ?? 25 }];
|
|
11
|
+
if (until) params[1].until = until;
|
|
12
|
+
const ac = new AbortController();
|
|
13
|
+
const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetchImpl(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "content-type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSignaturesForAddress", params }),
|
|
19
|
+
signal: ac.signal
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) throw new Error(`getSignaturesForAddress: HTTP ${res.status}`);
|
|
22
|
+
const body = await res.json();
|
|
23
|
+
if (body.error) throw new Error(`getSignaturesForAddress: ${body.error.message}`);
|
|
24
|
+
return body.result ?? [];
|
|
25
|
+
} finally {
|
|
26
|
+
clearTimeout(t);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function pollChainOnce(programId, state, opts = {}) {
|
|
30
|
+
const events = [];
|
|
31
|
+
const ts = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
32
|
+
let sigs;
|
|
33
|
+
try {
|
|
34
|
+
sigs = await getSignaturesForAddress(programId, state.lastSignature, opts);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
events.push({ type: "poll_error", programId, message: e?.message ?? String(e), ts: ts() });
|
|
37
|
+
return { events, state };
|
|
38
|
+
}
|
|
39
|
+
const probe = opts.probeAuthority ?? ((id, o) => probeUpgradeAuthority(id, o).then((ua) => ({ address: ua.address })));
|
|
40
|
+
let currentAuthority = state.baselineAuthority;
|
|
41
|
+
try {
|
|
42
|
+
const ua = await probe(programId, opts);
|
|
43
|
+
currentAuthority = ua.address;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
events.push({ type: "poll_error", programId, message: `authority probe: ${e?.message ?? String(e)}`, ts: ts() });
|
|
46
|
+
}
|
|
47
|
+
if (!state.initialized) {
|
|
48
|
+
const head = sigs[0]?.signature ?? null;
|
|
49
|
+
events.push({ type: "watch_started", programId, headSignature: head, baselineAuthority: currentAuthority, ts: ts() });
|
|
50
|
+
return {
|
|
51
|
+
events,
|
|
52
|
+
state: { lastSignature: head, baselineAuthority: currentAuthority, initialized: true }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (sigs.length > 0) {
|
|
56
|
+
events.push({
|
|
57
|
+
type: "new_activity",
|
|
58
|
+
programId,
|
|
59
|
+
newCount: sigs.length,
|
|
60
|
+
signatures: sigs.slice(0, 10).map((s) => s.signature),
|
|
61
|
+
ts: ts()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (currentAuthority !== state.baselineAuthority) {
|
|
65
|
+
events.push({
|
|
66
|
+
type: "authority_changed",
|
|
67
|
+
programId,
|
|
68
|
+
from: state.baselineAuthority,
|
|
69
|
+
to: currentAuthority,
|
|
70
|
+
ts: ts()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
events,
|
|
75
|
+
state: {
|
|
76
|
+
lastSignature: sigs[0]?.signature ?? state.lastSignature,
|
|
77
|
+
baselineAuthority: currentAuthority,
|
|
78
|
+
initialized: true
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function initialChainWatchState() {
|
|
83
|
+
return { lastSignature: null, baselineAuthority: null, initialized: false };
|
|
84
|
+
}
|
|
85
|
+
function startChainWatch(programId, opts = {}) {
|
|
86
|
+
const emit = opts.emit ?? ((e) => process.stdout.write(JSON.stringify(e) + "\n"));
|
|
87
|
+
const intervalMs = opts.intervalMs ?? 3e4;
|
|
88
|
+
let state = initialChainWatchState();
|
|
89
|
+
let stopped = false;
|
|
90
|
+
let timer;
|
|
91
|
+
const tick = async () => {
|
|
92
|
+
if (stopped) return;
|
|
93
|
+
const { events, state: next } = await pollChainOnce(programId, state, opts);
|
|
94
|
+
state = next;
|
|
95
|
+
for (const e of events) emit(e);
|
|
96
|
+
if (!stopped) timer = setTimeout(tick, intervalMs);
|
|
97
|
+
};
|
|
98
|
+
void tick();
|
|
99
|
+
return {
|
|
100
|
+
stop: () => {
|
|
101
|
+
stopped = true;
|
|
102
|
+
if (timer) clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
pollChainOnce,
|
|
109
|
+
initialChainWatchState,
|
|
110
|
+
startChainWatch
|
|
111
|
+
};
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
base58Encode
|
|
3
|
+
} from "./chunk-VG5FMOLW.js";
|
|
4
|
+
|
|
5
|
+
// src/firewall.ts
|
|
6
|
+
var KNOWN_PROGRAMS = {
|
|
7
|
+
"11111111111111111111111111111111": "System Program",
|
|
8
|
+
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "SPL Token",
|
|
9
|
+
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: "SPL Token-2022",
|
|
10
|
+
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "Associated Token Account",
|
|
11
|
+
metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s: "Metaplex Token Metadata",
|
|
12
|
+
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr: "SPL Memo",
|
|
13
|
+
ComputeBudget111111111111111111111111111111: "Compute Budget",
|
|
14
|
+
AddressLookupTab1e1111111111111111111111111: "Address Lookup Table",
|
|
15
|
+
BPFLoaderUpgradeab1e11111111111111111111111: "BPF Upgradeable Loader",
|
|
16
|
+
// Blue-chip DeFi / launch programs
|
|
17
|
+
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: "Jupiter Aggregator v6",
|
|
18
|
+
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium AMM v4",
|
|
19
|
+
whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: "Orca Whirlpools",
|
|
20
|
+
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "pump.fun",
|
|
21
|
+
pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA: "PumpSwap AMM"
|
|
22
|
+
};
|
|
23
|
+
var SPL_TOKEN_PROGRAMS = /* @__PURE__ */ new Set([
|
|
24
|
+
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
25
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
26
|
+
]);
|
|
27
|
+
var BPF_UPGRADEABLE_LOADER = "BPFLoaderUpgradeab1e11111111111111111111111";
|
|
28
|
+
function readCompactU16(buf, offset) {
|
|
29
|
+
let value = 0;
|
|
30
|
+
let shift = 0;
|
|
31
|
+
let o = offset;
|
|
32
|
+
for (; ; ) {
|
|
33
|
+
const byte = buf[o++];
|
|
34
|
+
value |= (byte & 127) << shift;
|
|
35
|
+
if ((byte & 128) === 0) break;
|
|
36
|
+
shift += 7;
|
|
37
|
+
if (shift > 21) throw new Error("compact-u16 too long");
|
|
38
|
+
}
|
|
39
|
+
return [value, o];
|
|
40
|
+
}
|
|
41
|
+
function decodeTransaction(base64, opts = {}) {
|
|
42
|
+
const bytes = new Uint8Array(Buffer.from(base64, "base64"));
|
|
43
|
+
if (bytes.length < 35) throw new Error("transaction too short to be valid");
|
|
44
|
+
let o = 0;
|
|
45
|
+
if (!opts.messageOnly) {
|
|
46
|
+
const [sigCount, afterCount] = readCompactU16(bytes, 0);
|
|
47
|
+
o = afterCount + sigCount * 64;
|
|
48
|
+
if (o >= bytes.length) throw new Error("signature array overruns buffer");
|
|
49
|
+
}
|
|
50
|
+
const msg = bytes.subarray(o);
|
|
51
|
+
let mo = 0;
|
|
52
|
+
let version = "legacy";
|
|
53
|
+
if ((msg[0] & 128) !== 0) {
|
|
54
|
+
version = msg[0] & 127;
|
|
55
|
+
mo = 1;
|
|
56
|
+
}
|
|
57
|
+
const numRequiredSignatures = msg[mo++];
|
|
58
|
+
const numReadonlySigned = msg[mo++];
|
|
59
|
+
const numReadonlyUnsigned = msg[mo++];
|
|
60
|
+
const [acctCount, afterAccts] = readCompactU16(msg, mo);
|
|
61
|
+
mo = afterAccts;
|
|
62
|
+
const staticAccountKeys = [];
|
|
63
|
+
for (let i = 0; i < acctCount; i++) {
|
|
64
|
+
staticAccountKeys.push(base58Encode(msg.subarray(mo, mo + 32)));
|
|
65
|
+
mo += 32;
|
|
66
|
+
}
|
|
67
|
+
const recentBlockhash = base58Encode(msg.subarray(mo, mo + 32));
|
|
68
|
+
mo += 32;
|
|
69
|
+
const [ixCount, afterIxCount] = readCompactU16(msg, mo);
|
|
70
|
+
mo = afterIxCount;
|
|
71
|
+
const instructions = [];
|
|
72
|
+
for (let i = 0; i < ixCount; i++) {
|
|
73
|
+
const programIdIndex = msg[mo++];
|
|
74
|
+
const [naccts, afterNaccts] = readCompactU16(msg, mo);
|
|
75
|
+
mo = afterNaccts;
|
|
76
|
+
const accountIndexes = [];
|
|
77
|
+
for (let j = 0; j < naccts; j++) accountIndexes.push(msg[mo++]);
|
|
78
|
+
const [datalen, afterDatalen] = readCompactU16(msg, mo);
|
|
79
|
+
mo = afterDatalen;
|
|
80
|
+
const data = msg.subarray(mo, mo + datalen);
|
|
81
|
+
mo += datalen;
|
|
82
|
+
instructions.push({
|
|
83
|
+
programIdIndex,
|
|
84
|
+
programId: staticAccountKeys[programIdIndex] ?? `#${programIdIndex}`,
|
|
85
|
+
accountIndexes,
|
|
86
|
+
data
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const addressTableLookups = [];
|
|
90
|
+
if (version !== "legacy") {
|
|
91
|
+
const [altCount, afterAlt] = readCompactU16(msg, mo);
|
|
92
|
+
mo = afterAlt;
|
|
93
|
+
for (let i = 0; i < altCount; i++) {
|
|
94
|
+
const accountKey = base58Encode(msg.subarray(mo, mo + 32));
|
|
95
|
+
mo += 32;
|
|
96
|
+
const [wlen, afterW] = readCompactU16(msg, mo);
|
|
97
|
+
mo = afterW + wlen;
|
|
98
|
+
const [rlen, afterR] = readCompactU16(msg, mo);
|
|
99
|
+
mo = afterR + rlen;
|
|
100
|
+
addressTableLookups.push({ accountKey, writableCount: wlen, readonlyCount: rlen });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
version,
|
|
105
|
+
numRequiredSignatures,
|
|
106
|
+
numReadonlySigned,
|
|
107
|
+
numReadonlyUnsigned,
|
|
108
|
+
staticAccountKeys,
|
|
109
|
+
recentBlockhash,
|
|
110
|
+
instructions,
|
|
111
|
+
addressTableLookups
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function splTokenFinding(disc) {
|
|
115
|
+
switch (disc) {
|
|
116
|
+
case 4:
|
|
117
|
+
// Approve
|
|
118
|
+
case 13:
|
|
119
|
+
return {
|
|
120
|
+
severity: "warn",
|
|
121
|
+
kind: "token-delegate-approval",
|
|
122
|
+
detail: "Approves a delegate over a token account \u2014 a common drain vector. Verify the delegate is trusted."
|
|
123
|
+
};
|
|
124
|
+
case 6:
|
|
125
|
+
return {
|
|
126
|
+
severity: "critical",
|
|
127
|
+
kind: "token-set-authority",
|
|
128
|
+
detail: "Changes a mint/account authority (SetAuthority). This can hand control of a token to another party."
|
|
129
|
+
};
|
|
130
|
+
case 9:
|
|
131
|
+
return {
|
|
132
|
+
severity: "info",
|
|
133
|
+
kind: "token-close-account",
|
|
134
|
+
detail: "Closes a token account and reclaims its rent to the destination."
|
|
135
|
+
};
|
|
136
|
+
default:
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function bpfLoaderFinding(disc) {
|
|
141
|
+
if (disc === 3)
|
|
142
|
+
return {
|
|
143
|
+
severity: "critical",
|
|
144
|
+
kind: "program-upgrade",
|
|
145
|
+
detail: "Upgrades a deployed program (BPF Upgradeable Loader Upgrade). Replaces on-chain code."
|
|
146
|
+
};
|
|
147
|
+
if (disc === 4 || disc === 6)
|
|
148
|
+
return {
|
|
149
|
+
severity: "critical",
|
|
150
|
+
kind: "program-set-authority",
|
|
151
|
+
detail: "Changes a program's upgrade authority (BPF Loader SetAuthority)."
|
|
152
|
+
};
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function analyzeInstructions(decoded, known) {
|
|
156
|
+
const findings = [];
|
|
157
|
+
for (const ix of decoded.instructions) {
|
|
158
|
+
const pid = ix.programId;
|
|
159
|
+
if (SPL_TOKEN_PROGRAMS.has(pid) && ix.data.length >= 1) {
|
|
160
|
+
const f = splTokenFinding(ix.data[0]);
|
|
161
|
+
if (f) findings.push(f);
|
|
162
|
+
}
|
|
163
|
+
if (pid === BPF_UPGRADEABLE_LOADER && ix.data.length >= 4) {
|
|
164
|
+
const disc = ix.data[0] | ix.data[1] << 8 | ix.data[2] << 16 | ix.data[3] << 24;
|
|
165
|
+
const f = bpfLoaderFinding(disc);
|
|
166
|
+
if (f) findings.push(f);
|
|
167
|
+
}
|
|
168
|
+
if (!(pid in known)) {
|
|
169
|
+
findings.push({
|
|
170
|
+
severity: "warn",
|
|
171
|
+
kind: "unknown-program",
|
|
172
|
+
detail: `Top-level instruction invokes an unrecognized program: ${pid}. Verify it is a program you intend to call.`
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (decoded.addressTableLookups.length > 0) {
|
|
177
|
+
findings.push({
|
|
178
|
+
severity: "info",
|
|
179
|
+
kind: "address-lookup-tables",
|
|
180
|
+
detail: `Transaction uses ${decoded.addressTableLookups.length} address lookup table(s); some accounts are resolved off-message and not statically visible.`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
function parseCpiPrograms(logs) {
|
|
186
|
+
const programs = /* @__PURE__ */ new Set();
|
|
187
|
+
for (const line of logs) {
|
|
188
|
+
const m = line.match(/^Program (\S+) invoke \[\d+\]$/);
|
|
189
|
+
if (m) programs.add(m[1]);
|
|
190
|
+
}
|
|
191
|
+
return [...programs];
|
|
192
|
+
}
|
|
193
|
+
async function simulate(base64, opts) {
|
|
194
|
+
const { DEFAULT_RPC } = await import("./rpc-W5F4KXS2.js");
|
|
195
|
+
const url = opts.rpcUrl ?? DEFAULT_RPC;
|
|
196
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
197
|
+
const ac = new AbortController();
|
|
198
|
+
const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetchImpl(url, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "content-type": "application/json" },
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
id: 1,
|
|
206
|
+
method: "simulateTransaction",
|
|
207
|
+
params: [base64, { encoding: "base64", sigVerify: false, replaceRecentBlockhash: true, commitment: "confirmed" }]
|
|
208
|
+
}),
|
|
209
|
+
signal: ac.signal
|
|
210
|
+
});
|
|
211
|
+
if (!res.ok) throw new Error(`simulateTransaction: HTTP ${res.status}`);
|
|
212
|
+
const body = await res.json();
|
|
213
|
+
if (body.error) throw new Error(`simulateTransaction: ${body.error.message}`);
|
|
214
|
+
const v = body.result?.value ?? {};
|
|
215
|
+
return { err: v.err ?? null, logs: v.logs ?? null, unitsConsumed: v.unitsConsumed };
|
|
216
|
+
} finally {
|
|
217
|
+
clearTimeout(t);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function worstSeverity(findings) {
|
|
221
|
+
if (findings.some((f) => f.severity === "critical")) return "block";
|
|
222
|
+
if (findings.some((f) => f.severity === "warn")) return "warn";
|
|
223
|
+
return "allow";
|
|
224
|
+
}
|
|
225
|
+
async function inspectTransaction(base64, opts = {}) {
|
|
226
|
+
const known = { ...KNOWN_PROGRAMS, ...opts.knownPrograms ?? {} };
|
|
227
|
+
const decoded = decodeTransaction(base64, { messageOnly: opts.messageOnly });
|
|
228
|
+
const findings = analyzeInstructions(decoded, known);
|
|
229
|
+
const topLevelIds = new Set(decoded.instructions.map((ix) => ix.programId));
|
|
230
|
+
const programs = [...topLevelIds].map((id) => ({
|
|
231
|
+
id,
|
|
232
|
+
label: known[id] ?? null,
|
|
233
|
+
known: id in known,
|
|
234
|
+
topLevel: true
|
|
235
|
+
}));
|
|
236
|
+
const report = {
|
|
237
|
+
version: decoded.version,
|
|
238
|
+
feePayer: decoded.staticAccountKeys[0] ?? "(none)",
|
|
239
|
+
numSigners: decoded.numRequiredSignatures,
|
|
240
|
+
staticAccounts: decoded.staticAccountKeys.length,
|
|
241
|
+
programs,
|
|
242
|
+
usesAddressLookupTables: decoded.addressTableLookups.length > 0,
|
|
243
|
+
simulation: { ran: false },
|
|
244
|
+
findings,
|
|
245
|
+
verdict: "allow"
|
|
246
|
+
};
|
|
247
|
+
if (opts.simulate !== false) {
|
|
248
|
+
try {
|
|
249
|
+
const sim = await simulate(base64, opts);
|
|
250
|
+
const cpiPrograms = sim.logs ? parseCpiPrograms(sim.logs) : [];
|
|
251
|
+
report.simulation = {
|
|
252
|
+
ran: true,
|
|
253
|
+
ok: sim.err === null,
|
|
254
|
+
err: sim.err ?? void 0,
|
|
255
|
+
unitsConsumed: sim.unitsConsumed,
|
|
256
|
+
logsCount: sim.logs?.length,
|
|
257
|
+
cpiPrograms
|
|
258
|
+
};
|
|
259
|
+
for (const pid of cpiPrograms) {
|
|
260
|
+
if (!(pid in known) && !topLevelIds.has(pid)) {
|
|
261
|
+
findings.push({
|
|
262
|
+
severity: "warn",
|
|
263
|
+
kind: "unknown-cpi-program",
|
|
264
|
+
detail: `Cross-program invocation to an unrecognized program: ${pid}.`
|
|
265
|
+
});
|
|
266
|
+
report.programs.push({ id: pid, label: null, known: false, topLevel: false });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (sim.err !== null) {
|
|
270
|
+
findings.push({
|
|
271
|
+
severity: "warn",
|
|
272
|
+
kind: "simulation-failed",
|
|
273
|
+
detail: `Transaction simulation returned an error: ${JSON.stringify(sim.err)}. Signing it would likely fail on-chain.`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
report.simulation = { ran: false };
|
|
278
|
+
findings.push({
|
|
279
|
+
severity: "info",
|
|
280
|
+
kind: "simulation-unavailable",
|
|
281
|
+
detail: `Could not simulate (static analysis only): ${e?.message ?? String(e)}`
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
report.verdict = worstSeverity(findings);
|
|
286
|
+
return report;
|
|
287
|
+
}
|
|
288
|
+
var SEV_ICON = { critical: "\u26D4", warn: "\u26A0 ", info: "\xB7 " };
|
|
289
|
+
var VERDICT_BANNER = {
|
|
290
|
+
allow: "ALLOW \u2014 no dangerous patterns detected",
|
|
291
|
+
warn: "WARN \u2014 review before signing",
|
|
292
|
+
block: "BLOCK \u2014 dangerous pattern detected, do not sign blind"
|
|
293
|
+
};
|
|
294
|
+
function renderFirewallText(r) {
|
|
295
|
+
const lines = [];
|
|
296
|
+
lines.push(`Transaction firewall [${VERDICT_BANNER[r.verdict]}]`);
|
|
297
|
+
lines.push("");
|
|
298
|
+
lines.push(` Version: ${r.version}`);
|
|
299
|
+
lines.push(` Fee payer: ${r.feePayer}`);
|
|
300
|
+
lines.push(` Signers: ${r.numSigners}`);
|
|
301
|
+
lines.push(` Accounts: ${r.staticAccounts}${r.usesAddressLookupTables ? " (+ lookup tables)" : ""}`);
|
|
302
|
+
lines.push(` Programs:`);
|
|
303
|
+
for (const p of r.programs) {
|
|
304
|
+
const tag = p.known ? p.label : "UNKNOWN";
|
|
305
|
+
lines.push(` ${p.known ? "\u2713" : "?"} ${p.id} ${tag}${p.topLevel ? "" : " (via CPI)"}`);
|
|
306
|
+
}
|
|
307
|
+
if (r.simulation.ran) {
|
|
308
|
+
lines.push(` Simulation: ${r.simulation.ok ? "ok" : "FAILED"}${r.simulation.unitsConsumed != null ? ` (${r.simulation.unitsConsumed} CU)` : ""}`);
|
|
309
|
+
} else {
|
|
310
|
+
lines.push(` Simulation: not run`);
|
|
311
|
+
}
|
|
312
|
+
lines.push("");
|
|
313
|
+
if (r.findings.length === 0) {
|
|
314
|
+
lines.push(" No findings.");
|
|
315
|
+
} else {
|
|
316
|
+
lines.push(" Findings:");
|
|
317
|
+
for (const f of r.findings) {
|
|
318
|
+
lines.push(` ${SEV_ICON[f.severity]} [${f.kind}] ${f.detail}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export {
|
|
325
|
+
KNOWN_PROGRAMS,
|
|
326
|
+
decodeTransaction,
|
|
327
|
+
analyzeInstructions,
|
|
328
|
+
parseCpiPrograms,
|
|
329
|
+
inspectTransaction,
|
|
330
|
+
renderFirewallText
|
|
331
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/idlRules.ts
|
|
2
|
+
import { stringify as yamlStringify } from "yaml";
|
|
3
|
+
function toSnakeCase(s) {
|
|
4
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
function idlProgramName(idl) {
|
|
7
|
+
return idl.metadata?.name ?? idl.name ?? "anchor-program";
|
|
8
|
+
}
|
|
9
|
+
function flattenAccounts(accounts) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const a of accounts) {
|
|
12
|
+
if (a.accounts && a.accounts.length > 0) {
|
|
13
|
+
out.push(...flattenAccounts(a.accounts));
|
|
14
|
+
} else {
|
|
15
|
+
out.push(a);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function parseIdl(json) {
|
|
21
|
+
if (!json || typeof json !== "object") throw new Error("IDL is not an object");
|
|
22
|
+
const idl = json;
|
|
23
|
+
if (!Array.isArray(idl.instructions)) throw new Error("IDL has no instructions array");
|
|
24
|
+
for (const ix of idl.instructions) {
|
|
25
|
+
if (!ix.name || !Array.isArray(ix.accounts)) {
|
|
26
|
+
throw new Error(`IDL instruction missing name/accounts: ${JSON.stringify(ix).slice(0, 80)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return idl;
|
|
30
|
+
}
|
|
31
|
+
function buildConstraintParams(idl) {
|
|
32
|
+
const instructions = idl.instructions.map((ix) => {
|
|
33
|
+
const leaves = flattenAccounts(ix.accounts);
|
|
34
|
+
return {
|
|
35
|
+
name: toSnakeCase(ix.name),
|
|
36
|
+
signers: leaves.filter((a) => a.isSigner).map((a) => toSnakeCase(a.name)),
|
|
37
|
+
mutable: leaves.filter((a) => a.isMut).map((a) => toSnakeCase(a.name))
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return { idlName: idlProgramName(idl), instructions };
|
|
41
|
+
}
|
|
42
|
+
function generateRulesFromIdl(idl) {
|
|
43
|
+
const params = buildConstraintParams(idl);
|
|
44
|
+
const handlerNames = params.instructions.map((i) => i.name).filter(Boolean);
|
|
45
|
+
if (handlerNames.length === 0) return [];
|
|
46
|
+
const nameRegex = `^(${handlerNames.map(escapeRegex).join("|")})$`;
|
|
47
|
+
const progName = params.idlName;
|
|
48
|
+
const rule = {
|
|
49
|
+
id: `idl-${toKebab(progName)}-account-constraints`,
|
|
50
|
+
severity: "critical",
|
|
51
|
+
title: `Anchor accounts match the ${progName} IDL signer/mut constraints`,
|
|
52
|
+
component: {
|
|
53
|
+
name: progName,
|
|
54
|
+
type: "Anchor program",
|
|
55
|
+
version: idl.metadata?.version ?? idl.version ?? "unversioned",
|
|
56
|
+
sourceUrl: "https://www.anchor-lang.com/docs/the-accounts-struct"
|
|
57
|
+
},
|
|
58
|
+
detect: {
|
|
59
|
+
lang: "rust",
|
|
60
|
+
modules: ["@coral-xyz/anchor", "@project-serum/anchor"],
|
|
61
|
+
nameRegex,
|
|
62
|
+
triggerCalls: []
|
|
63
|
+
},
|
|
64
|
+
check: {
|
|
65
|
+
kind: "anchor-account-matches-idl",
|
|
66
|
+
params
|
|
67
|
+
},
|
|
68
|
+
test: { kind: "none" }
|
|
69
|
+
};
|
|
70
|
+
return [rule];
|
|
71
|
+
}
|
|
72
|
+
function escapeRegex(s) {
|
|
73
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
74
|
+
}
|
|
75
|
+
function toKebab(s) {
|
|
76
|
+
return toSnakeCase(s).replace(/_/g, "-");
|
|
77
|
+
}
|
|
78
|
+
function renderRulesYaml(rules) {
|
|
79
|
+
return rules.map((r) => yamlStringify(r)).join("\n---\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
toSnakeCase,
|
|
84
|
+
idlProgramName,
|
|
85
|
+
parseIdl,
|
|
86
|
+
buildConstraintParams,
|
|
87
|
+
generateRulesFromIdl,
|
|
88
|
+
renderRulesYaml
|
|
89
|
+
};
|