@finchagentic/mcp 4.6.2 → 4.6.4

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.
Files changed (59) hide show
  1. package/README.md +39 -74
  2. package/dist/_http-cache.js +96 -0
  3. package/dist/_text-search.js +39 -0
  4. package/dist/agent-loop.js +301 -0
  5. package/dist/annotations.js +122 -0
  6. package/dist/cli.js +1391 -0
  7. package/dist/clink-input.js +15 -0
  8. package/dist/config.js +132 -0
  9. package/dist/convex.js +175 -0
  10. package/dist/dex-pair.js +54 -0
  11. package/dist/enrichment-router.js +315 -0
  12. package/dist/index.js +258 -0
  13. package/dist/llm.js +298 -0
  14. package/dist/local-memory-file.js +150 -0
  15. package/dist/local-memory.js +135 -0
  16. package/dist/local-vault.js +456 -0
  17. package/dist/output-schemas.js +605 -0
  18. package/dist/project.js +36 -0
  19. package/dist/prompts.js +111 -0
  20. package/dist/public-url.js +107 -0
  21. package/dist/resources.js +111 -0
  22. package/dist/server.js +322 -0
  23. package/dist/signal-gate.js +57 -0
  24. package/dist/token-decimals.js +26 -0
  25. package/dist/token-gate.js +88 -0
  26. package/dist/tool-filter.js +53 -0
  27. package/dist/tools/_solidity-scan.js +313 -0
  28. package/dist/tools/agents.js +441 -0
  29. package/dist/tools/automation.js +354 -0
  30. package/dist/tools/base-mcp.js +466 -0
  31. package/dist/tools/base.js +283 -0
  32. package/dist/tools/chronicle.js +268 -0
  33. package/dist/tools/coder.js +94 -0
  34. package/dist/tools/deep-research.js +1421 -0
  35. package/dist/tools/defi.js +292 -0
  36. package/dist/tools/equity.js +372 -0
  37. package/dist/tools/events.js +182 -0
  38. package/dist/tools/github.js +564 -0
  39. package/dist/tools/insider.js +264 -0
  40. package/dist/tools/insight.js +630 -0
  41. package/dist/tools/market.js +555 -0
  42. package/dist/tools/memory.js +1059 -0
  43. package/dist/tools/miroshark.js +350 -0
  44. package/dist/tools/monitor.js +319 -0
  45. package/dist/tools/os.js +236 -0
  46. package/dist/tools/packets.js +296 -0
  47. package/dist/tools/research-chain.js +226 -0
  48. package/dist/tools/research-compare.js +280 -0
  49. package/dist/tools/research.js +188 -0
  50. package/dist/tools/rh-bridge.js +148 -0
  51. package/dist/tools/rh-mcp.js +1448 -0
  52. package/dist/tools/rh-orders.js +556 -0
  53. package/dist/tools/scanner.js +564 -0
  54. package/dist/tools/stake.js +369 -0
  55. package/dist/tools/vault.js +1020 -0
  56. package/dist/tools/wallet.js +200 -0
  57. package/dist/types.js +2 -0
  58. package/dist/wallet.js +372 -0
  59. package/package.json +4 -7
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ // Insufficient-signal detector used by deep_research before saving to vault.
3
+ // Mirrors convex/_signalGate.ts in the app - same logic, different runtime.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.checkSignal = checkSignal;
6
+ const META_FAILURE_PHRASES = [
7
+ "search returned directory",
8
+ "directory pages",
9
+ "topic landing pages",
10
+ "landing pages from major",
11
+ "aggregator homepages",
12
+ "index pages",
13
+ "results provide no",
14
+ "no specific findings",
15
+ "no specific developments",
16
+ "no specific announcements",
17
+ "no specific stories",
18
+ "no concrete findings",
19
+ "no substantive findings",
20
+ "no specific breakthroughs",
21
+ "no specific data",
22
+ "rather than current stories",
23
+ "rather than specific",
24
+ "rather than individual articles",
25
+ "insufficient data to",
26
+ "search results were thin",
27
+ ];
28
+ function checkSignal(content) {
29
+ const text = (content ?? "").trim();
30
+ if (text.length < 300) {
31
+ return { ok: false, reason: "output too short (<300 chars)", score: 0 };
32
+ }
33
+ const lower = text.toLowerCase();
34
+ for (const phrase of META_FAILURE_PHRASES) {
35
+ if (lower.includes(phrase)) {
36
+ return {
37
+ ok: false,
38
+ reason: `search returned only metadata pages - LLM flagged it ("${phrase}")`,
39
+ score: 0.2,
40
+ };
41
+ }
42
+ }
43
+ const numbers = (text.match(/\b\d{1,4}([.,]\d+)?\s*(%|USD|usd|m|M|k|K|bn|B|x|gwei|eth|ETH|btc|BTC)?\b/g) ?? []).length;
44
+ const links = (text.match(/https?:\/\//g) ?? []).length;
45
+ const quotes = (text.match(/"[^"]{10,}"/g) ?? []).length;
46
+ if (text.length < 800 && numbers < 3 && links < 1 && quotes < 1) {
47
+ return {
48
+ ok: false,
49
+ reason: "low concrete-data density (no numbers, links, or quoted findings)",
50
+ score: 0.3,
51
+ };
52
+ }
53
+ const lengthScore = Math.min(1, text.length / 2000);
54
+ const dataScore = Math.min(1, (numbers + links * 2 + quotes * 2) / 25);
55
+ const score = Math.max(0, Math.min(1, lengthScore * 0.4 + dataScore * 0.6));
56
+ return { ok: true, score };
57
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ // Canonical token decimals for the MCP server — ONE source of truth.
3
+ // Mirrors app/convex/tokenDecimals.ts. Keep the two in sync when adding a token;
4
+ // they are separate packages so the module itself cannot be shared.
5
+ //
6
+ // Previously duplicated across tools/defi.ts and tools/base-mcp.ts with a silent
7
+ // `?? 18` fallback. Assuming 18 decimals for a 6-decimal token misreports the
8
+ // amount by 10^12, so unknown tokens return undefined and callers must say so.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TOKEN_DECIMALS = void 0;
11
+ exports.decimalsFor = decimalsFor;
12
+ exports.TOKEN_DECIMALS = {
13
+ ETH: 18,
14
+ WETH: 18,
15
+ DAI: 18,
16
+ FINCH: 18,
17
+ USDC: 6,
18
+ USDT: 6,
19
+ CBBTC: 8,
20
+ };
21
+ /** Decimals for a token symbol, or undefined if unknown. Never guesses. */
22
+ function decimalsFor(symbol) {
23
+ if (!symbol)
24
+ return undefined;
25
+ return exports.TOKEN_DECIMALS[symbol.trim().toUpperCase()];
26
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PREMIUM_TOOLS = void 0;
4
+ exports.getTier = getTier;
5
+ exports.tokenGateError = tokenGateError;
6
+ const wallet_js_1 = require("./wallet.js");
7
+ const config_js_1 = require("./config.js");
8
+ const FINCH_TOKEN_CA = "0x4B524015D54a27d4472F5c59c570730D69499Ba3";
9
+ const BALANCE_SELECTOR = "0x70a08231"; // balanceOf(address)
10
+ const CACHE_TTL = 5 * 60 * 1000; // 5 min - avoid per-call RPC
11
+ // 1 FINCH (18 decimals). Override via FINCH_MIN_BALANCE env var.
12
+ const MIN_BALANCE = BigInt(process.env.FINCH_MIN_BALANCE ?? "1000000000000000000");
13
+ let _cache = null;
14
+ async function erc20BalanceOf(address) {
15
+ const padded = address.toLowerCase().replace("0x", "").padStart(64, "0");
16
+ const res = await fetch(wallet_js_1.BASE_RPC, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({
20
+ jsonrpc: "2.0", id: 1, method: "eth_call",
21
+ params: [{ to: FINCH_TOKEN_CA, data: BALANCE_SELECTOR + padded }, "latest"],
22
+ }),
23
+ signal: AbortSignal.timeout(8000),
24
+ });
25
+ const data = await res.json();
26
+ return BigInt(data.result ?? "0x0");
27
+ }
28
+ // Session token or API key → always holder (authenticated user)
29
+ function hasAuthBypass() {
30
+ return !!((0, config_js_1.getSavedToken)() || process.env.FINCH_API_KEY);
31
+ }
32
+ async function getTier() {
33
+ if (hasAuthBypass())
34
+ return "holder";
35
+ if (_cache && Date.now() - _cache.at < CACHE_TTL)
36
+ return _cache.tier;
37
+ try {
38
+ const wallet = await (0, wallet_js_1.getOrCreateWallet)();
39
+ const balance = await erc20BalanceOf(wallet.address);
40
+ const tier = balance >= MIN_BALANCE ? "holder" : "basic";
41
+ _cache = { tier, at: Date.now() };
42
+ return tier;
43
+ }
44
+ catch {
45
+ // RPC unreachable - don't block, degrade gracefully
46
+ return "basic";
47
+ }
48
+ }
49
+ // Tools that require FINCH token to unlock
50
+ exports.PREMIUM_TOOLS = new Set([
51
+ // Simulation
52
+ "miroshark_simulate", "miroshark_status", "miroshark_stop",
53
+ // AI analysis
54
+ "market_thesis", "trade_plan",
55
+ // Advanced memory
56
+ "memory_insight", "memory_extract", "memory_consolidate",
57
+ // Automations
58
+ "create_automation", "run_automation", "pause_automation", "delete_automation", "get_automation_runs",
59
+ // Autonomous monitors
60
+ "schedule_research",
61
+ // Persistent agents
62
+ "agent_spawn", "agent_recall", "agent_update",
63
+ ]);
64
+ function tokenGateError(toolName) {
65
+ return {
66
+ content: [{
67
+ type: "text",
68
+ text: [
69
+ "🔒 **Premium Tool**",
70
+ "",
71
+ `\`${toolName}\` requires a Finch account or FINCH token.`,
72
+ "",
73
+ "**Option 1 - Sign in (easiest):**",
74
+ "1. Go to finchagentic.com and sign in",
75
+ "2. Copy your session token from Settings",
76
+ "3. Add to your MCP config: `FINCH_SESSION_TOKEN=…`",
77
+ "",
78
+ "**Option 2 - Hold FINCH token on Base:**",
79
+ "1. Get FINCH - CA: `0x4B524015D54a27d4472F5c59c570730D69499Ba3`",
80
+ "2. Hold at least 1 FINCH in your local wallet",
81
+ "3. Access unlocks automatically",
82
+ "",
83
+ "Run `finch_status` to check your current tier.",
84
+ ].join("\n"),
85
+ }],
86
+ isError: true,
87
+ };
88
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filterTools = filterTools;
4
+ // Tool-subset filter. Each user keeps the full handler map (so any tool
5
+ // can still be invoked by name if explicitly referenced), but the LIST
6
+ // response sent to MCP clients is trimmed based on FINCH_TOOLS.
7
+ //
8
+ // Default = "core" - runtime essentials only (memory, vault, agents,
9
+ // chronicle, status). Opt-in presets let token-conscious users cut LLM
10
+ // context cost while keeping the full surface accessible by name:
11
+ //
12
+ // FINCH_TOOLS=core runtime essentials (memory, vault, agents)
13
+ // FINCH_TOOLS=defi Base + market + DeFi execution
14
+ // FINCH_TOOLS=research research + memory + vault
15
+ // FINCH_TOOLS=memory memory + vault + agents only
16
+ // FINCH_TOOLS=memory,defi comma-separated combination
17
+ // FINCH_TOOLS=all every registered tool
18
+ //
19
+ // Unknown presets fall back to all to avoid silently hiding tools.
20
+ // Each preset's regex is checked against every tool name in ALL_TOOLS by
21
+ // tools-registration.test.ts (see "every tool matches at least one preset")
22
+ // - a tool that matches no preset's regex is invisible under every FINCH_TOOLS
23
+ // setting except "all", which happened silently to 33/121 tools before that
24
+ // test existed (rh_* order-management tools, all *_automation tools, packets,
25
+ // playbooks, wallet balance/sign, miroshark, stock_* - a user on
26
+ // FINCH_TOOLS=defi couldn't even see rh_order_cancel for an order placed
27
+ // through that same preset). New tools MUST match a preset or the test fails
28
+ // the build, instead of quietly vanishing like these did.
29
+ const PRESETS = {
30
+ core: /^(memory_|vault_|code_session_save|list_projects|agent_|ask_finch|finch_status|finch_diagnostics|finch_shell_chat|get_wallet_address|get_wallet_balance|wallet_sign_message|chronicle_|packet_)/,
31
+ defi: /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history|get_base_token_data|stock_fundamentals|stock_insider|stock_events|market_thesis|trade_plan|base_mcp_|rh_|base_|get_defi_yields|score_token|check_token|scan_market|get_wallet_balance|wallet_sign_message|create_automation|list_automations|pause_automation|delete_automation|get_automation_runs|run_automation|miroshark_|stake_|unstake_finch|claim_vested_rewards)/,
32
+ research: /^(memory_|vault_|code_session_save|list_projects|deep_research|research_compare|research_chain|web_search|web_scrape|schedule_research|list_monitors|cancel_monitor|ask_finch|stock_fundamentals|stock_insider|stock_events)/,
33
+ memory: /^(memory_|vault_|code_session_save|list_projects|agent_|chronicle_)/,
34
+ coder: /^(audit_contract|github_|code_session_save)/,
35
+ };
36
+ function filterTools(allTools) {
37
+ // Default is "core" - keeps LLM context cost low while everything
38
+ // is still callable by name. Power users opt back in via
39
+ // FINCH_TOOLS=all. Explicit empty env still means "all" for back-compat.
40
+ const raw = (process.env.FINCH_TOOLS ?? "core").trim().toLowerCase();
41
+ const env = raw === "" ? "core" : raw;
42
+ if (env === "all")
43
+ return allTools;
44
+ const presetKeys = env.split(",").map((s) => s.trim()).filter(Boolean);
45
+ const patterns = presetKeys
46
+ .map((k) => PRESETS[k])
47
+ .filter((p) => !!p);
48
+ if (patterns.length === 0) {
49
+ // Unknown preset(s) - surface full tool set rather than silently hide.
50
+ return allTools;
51
+ }
52
+ return allTools.filter((t) => patterns.some((p) => p.test(t.name)));
53
+ }
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ // Static heuristic scanner for Solidity audit grounding.
3
+ //
4
+ // The LLM-only audit path was dangerous: a model could say "this contract
5
+ // looks safe" without any structural basis, and a user might trust that
6
+ // claim with real funds. This module runs a regex/pattern scan over the
7
+ // source BEFORE the LLM call and forces the LLM to address each finding
8
+ // (either confirm the risk or explain why it's a false positive). The
9
+ // resulting report leads with "automated static checks", then "LLM review
10
+ // over those findings", then a mandatory disclaimer.
11
+ //
12
+ // This is NOT a substitute for a professional audit. It catches obvious
13
+ // antipatterns. Subtle vulnerabilities (reentrancy guarded by state ordering,
14
+ // signature replay, oracle manipulation under non-obvious conditions) still
15
+ // need human review or specialized tooling (Slither, Mythril, Echidna).
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.AUDIT_DISCLAIMER = void 0;
18
+ exports.staticScanSolidity = staticScanSolidity;
19
+ exports.formatFindings = formatFindings;
20
+ const PATTERNS = [
21
+ // ─── Critical ─────────────────────────────────────────────────────────────
22
+ {
23
+ id: "SOL-TX-ORIGIN",
24
+ severity: "critical",
25
+ title: "tx.origin used for authorization",
26
+ detail: "tx.origin returns the original EOA that started the call chain - vulnerable to phishing where a malicious contract relays calls. Use msg.sender for auth.",
27
+ regex: /\btx\.origin\b/,
28
+ reference: "https://docs.soliditylang.org/en/latest/security-considerations.html#tx-origin",
29
+ },
30
+ {
31
+ id: "SOL-DELEGATECALL",
32
+ severity: "critical",
33
+ title: "delegatecall to a non-immutable target",
34
+ detail: "delegatecall executes target code in caller's storage. If target is a state variable or function parameter it can be hijacked to overwrite storage or steal funds.",
35
+ customScan: (lines) => {
36
+ const out = [];
37
+ lines.forEach((l, i) => {
38
+ if (/\.delegatecall\s*\(/i.test(l) && !/immutable|constant/.test(l)) {
39
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
40
+ }
41
+ });
42
+ return out;
43
+ },
44
+ reference: "https://docs.soliditylang.org/en/latest/security-considerations.html#use-the-checks-effects-interactions-pattern",
45
+ },
46
+ {
47
+ id: "SOL-SELFDESTRUCT",
48
+ severity: "critical",
49
+ title: "selfdestruct present",
50
+ detail: "selfdestruct (renamed `selfdestruct` in 0.8.x; deprecated post-Cancun) wipes contract code. Requires strict access control. Note: Cancun changed semantics - most uses should be removed entirely.",
51
+ regex: /\bselfdestruct\s*\(|suicide\s*\(/,
52
+ },
53
+ // ─── High ─────────────────────────────────────────────────────────────────
54
+ {
55
+ id: "SOL-REENTRANCY-PATTERN",
56
+ severity: "high",
57
+ title: "External call before state mutation (reentrancy risk)",
58
+ detail: "Heuristic detects `.call{value:` or `.transfer(` followed by storage writes in the next ~15 lines. Confirm Checks-Effects-Interactions ordering or ReentrancyGuard usage.",
59
+ customScan: (lines) => {
60
+ const out = [];
61
+ lines.forEach((l, i) => {
62
+ if (/\.call\s*\{|\.transfer\s*\(|\.send\s*\(/.test(l) && !/^\s*\/\//.test(l)) {
63
+ // Look ahead for storage write
64
+ const slice = lines.slice(i + 1, Math.min(i + 16, lines.length)).join("\n");
65
+ if (/^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*=|^\s*[a-zA-Z_][a-zA-Z0-9_]*\[[^\]]*\]\s*=|\.push\s*\(|\.pop\s*\(|delete\s+/m.test(slice)) {
66
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
67
+ }
68
+ }
69
+ });
70
+ return out;
71
+ },
72
+ reference: "https://swcregistry.io/docs/SWC-107",
73
+ },
74
+ {
75
+ id: "SOL-UNCHECKED-CALL",
76
+ severity: "high",
77
+ title: "Low-level call return value not checked",
78
+ detail: "`.call(...)` and `.delegatecall(...)` return (bool, bytes). Ignoring the bool means failures silently pass.",
79
+ customScan: (lines) => {
80
+ const out = [];
81
+ lines.forEach((l, i) => {
82
+ if (/\.call\s*\{|\.call\s*\(|\.delegatecall\s*\(/.test(l)) {
83
+ // Has assignment / require / boolean check?
84
+ const hasCheck = /\(bool|=\s*(?:address|payable)?[a-zA-Z_]/i.test(l)
85
+ || /require\s*\(/.test(l)
86
+ || /\(\s*bool\s+/i.test(l);
87
+ if (!hasCheck)
88
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
89
+ }
90
+ });
91
+ return out;
92
+ },
93
+ reference: "https://swcregistry.io/docs/SWC-104",
94
+ },
95
+ {
96
+ id: "SOL-FLOATING-PRAGMA",
97
+ severity: "medium",
98
+ title: "Floating pragma",
99
+ detail: "Using `^0.8.x` lets the contract compile with any minor version - different compilers can introduce subtle behavior changes. Pin to a specific version for production.",
100
+ regex: /pragma\s+solidity\s+\^/i,
101
+ },
102
+ {
103
+ id: "SOL-BLOCK-TIMESTAMP",
104
+ severity: "medium",
105
+ title: "block.timestamp used in conditional logic",
106
+ detail: "Miners can shift block.timestamp by ~15s. Acceptable for long timeouts; dangerous for randomness, short deadlines, or precise time-locks.",
107
+ regex: /\bblock\.timestamp\b|\bnow\b/,
108
+ reference: "https://swcregistry.io/docs/SWC-116",
109
+ },
110
+ {
111
+ id: "SOL-BLOCK-NUMBER-RAND",
112
+ severity: "high",
113
+ title: "block.number / blockhash used as randomness source",
114
+ detail: "block.number is predictable; blockhash returns 0 for blocks older than 256. Use Chainlink VRF or commit-reveal for fair randomness.",
115
+ customScan: (lines) => {
116
+ const out = [];
117
+ lines.forEach((l, i) => {
118
+ if (/\b(?:blockhash|block\.difficulty|block\.prevrandao)\s*\(/.test(l)
119
+ && /\b(?:random|rand|seed|lottery)/i.test(l)) {
120
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
121
+ }
122
+ });
123
+ return out;
124
+ },
125
+ },
126
+ // ─── Medium ───────────────────────────────────────────────────────────────
127
+ {
128
+ id: "SOL-MISSING-ZERO-CHECK",
129
+ severity: "low",
130
+ title: "Address parameter without zero-address check (heuristic)",
131
+ detail: "Setter functions that store an address but don't `require(addr != address(0))` can permanently break the contract. Heuristic - verify whether your intent allows zero.",
132
+ customScan: (lines) => {
133
+ const out = [];
134
+ lines.forEach((l, i) => {
135
+ // Match `function setX(address _x)` or similar
136
+ const fn = l.match(/function\s+set\w*\s*\(\s*address\s+/);
137
+ if (fn) {
138
+ const body = lines.slice(i, Math.min(i + 12, lines.length)).join("\n");
139
+ if (!/require\s*\([^)]*address\s*\(\s*0\s*\)|!=\s*address\s*\(\s*0\s*\)/.test(body)) {
140
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
141
+ }
142
+ }
143
+ });
144
+ return out;
145
+ },
146
+ },
147
+ {
148
+ id: "SOL-PUBLIC-MUTATING",
149
+ severity: "low",
150
+ title: "Public function with no access modifier (heuristic)",
151
+ detail: "External functions that mutate state with no onlyOwner/AccessControl modifier may be unintentionally open. Verify intent.",
152
+ customScan: (lines) => {
153
+ const out = [];
154
+ lines.forEach((l, i) => {
155
+ // function ... public ... { but no onlyOwner / require( msg.sender on next few lines
156
+ if (/function\s+\w+\s*\([^)]*\)\s+(?:public|external)\s/.test(l)
157
+ && !/view|pure/.test(l)
158
+ && !/onlyOwner|onlyRole|onlyAdmin|nonReentrant/.test(l)) {
159
+ const body = lines.slice(i, Math.min(i + 4, lines.length)).join("\n");
160
+ if (!/require\s*\(\s*msg\.sender|onlyOwner|onlyRole|AccessControl/.test(body)) {
161
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
162
+ }
163
+ }
164
+ });
165
+ return out;
166
+ },
167
+ },
168
+ {
169
+ id: "SOL-UNBOUNDED-LOOP",
170
+ severity: "medium",
171
+ title: "Loop over array of unbounded length",
172
+ detail: "for/while loops over arrays that anyone can grow are gas-bomb vectors and DoS risks.",
173
+ customScan: (lines) => {
174
+ const out = [];
175
+ lines.forEach((l, i) => {
176
+ if (/for\s*\(.*<\s*[a-zA-Z_][a-zA-Z0-9_]*\.length/.test(l)
177
+ && !/i\s*<\s*\d+/.test(l)) {
178
+ out.push({ line: i + 1, matched: l.trim().slice(0, 200) });
179
+ }
180
+ });
181
+ return out;
182
+ },
183
+ },
184
+ // ─── Info ─────────────────────────────────────────────────────────────────
185
+ {
186
+ id: "SOL-NO-EVENTS",
187
+ severity: "info",
188
+ title: "State-mutating function may not emit events",
189
+ detail: "Critical state changes should emit events for off-chain indexers and auditability.",
190
+ customScan: (lines) => {
191
+ // Crude heuristic - only flag if zero `emit` statements in entire file
192
+ const hasEmit = lines.some((l) => /\bemit\s+\w+\s*\(/.test(l));
193
+ if (hasEmit)
194
+ return [];
195
+ // Find first state-changing fn for the report
196
+ for (let i = 0; i < lines.length; i++) {
197
+ if (/function\s+\w+\s*\([^)]*\)\s+(?:public|external)\s/.test(lines[i])
198
+ && !/view|pure/.test(lines[i])) {
199
+ return [{ line: i + 1, matched: lines[i].trim().slice(0, 200) }];
200
+ }
201
+ }
202
+ return [];
203
+ },
204
+ },
205
+ {
206
+ id: "SOL-NO-LICENSE",
207
+ severity: "info",
208
+ title: "Missing SPDX license identifier",
209
+ detail: "Solidity emits a warning when no SPDX comment is present. Required for clean compile in CI.",
210
+ customScan: (lines) => {
211
+ const hasLicense = lines.slice(0, 10).some((l) => /SPDX-License-Identifier:/.test(l));
212
+ return hasLicense ? [] : [{ line: 1, matched: lines[0]?.trim().slice(0, 200) ?? "(file start)" }];
213
+ },
214
+ },
215
+ ];
216
+ /**
217
+ * Run all patterns over a Solidity source string. Returns flat list of
218
+ * findings sorted by severity (critical first).
219
+ */
220
+ function staticScanSolidity(source) {
221
+ const lines = source.split(/\r?\n/);
222
+ const findings = [];
223
+ for (const p of PATTERNS) {
224
+ if (p.regex) {
225
+ lines.forEach((line, idx) => {
226
+ const trimmed = line.trim();
227
+ // Skip pure comment lines
228
+ if (/^(\/\/|\*|\/\*)/.test(trimmed))
229
+ return;
230
+ if (p.regex.test(line)) {
231
+ findings.push({
232
+ id: p.id, severity: p.severity, title: p.title, detail: p.detail,
233
+ lineHint: idx + 1, matched: line.trim().slice(0, 200),
234
+ reference: p.reference,
235
+ });
236
+ }
237
+ });
238
+ }
239
+ else if (p.customScan) {
240
+ const hits = p.customScan(lines);
241
+ for (const h of hits) {
242
+ findings.push({
243
+ id: p.id, severity: p.severity, title: p.title, detail: p.detail,
244
+ lineHint: h.line, matched: h.matched,
245
+ reference: p.reference,
246
+ });
247
+ }
248
+ }
249
+ }
250
+ // Dedup: same id + same line = one entry
251
+ const seen = new Set();
252
+ const deduped = findings.filter((f) => {
253
+ const k = `${f.id}:${f.lineHint ?? "0"}`;
254
+ if (seen.has(k))
255
+ return false;
256
+ seen.add(k);
257
+ return true;
258
+ });
259
+ const sevRank = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
260
+ return deduped.sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
261
+ }
262
+ /**
263
+ * Format findings as a markdown block to inject into the LLM prompt or
264
+ * into the final report.
265
+ */
266
+ function formatFindings(findings) {
267
+ if (findings.length === 0) {
268
+ return "_No common antipatterns detected by static scan. This does not mean the contract is safe - subtle issues still need human review._";
269
+ }
270
+ const byCat = { critical: [], high: [], medium: [], low: [], info: [] };
271
+ for (const f of findings)
272
+ byCat[f.severity].push(f);
273
+ const sections = [];
274
+ const labels = {
275
+ critical: "🚨 CRITICAL", high: "🔴 HIGH", medium: "🟡 MEDIUM", low: "🔵 LOW", info: "ℹ️ INFO",
276
+ };
277
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
278
+ if (byCat[sev].length === 0)
279
+ continue;
280
+ sections.push(`### ${labels[sev]} (${byCat[sev].length})`);
281
+ for (const f of byCat[sev]) {
282
+ sections.push([
283
+ `- **${f.title}** \`${f.id}\``,
284
+ f.lineHint ? ` - Line ${f.lineHint}: \`${f.matched}\`` : "",
285
+ ` - ${f.detail}`,
286
+ f.reference ? ` - Reference: ${f.reference}` : "",
287
+ ].filter(Boolean).join("\n"));
288
+ }
289
+ sections.push("");
290
+ }
291
+ return sections.join("\n");
292
+ }
293
+ /**
294
+ * Mandatory disclaimer appended to every audit report. Sets expectations
295
+ * so users don't treat the output as a professional audit.
296
+ */
297
+ exports.AUDIT_DISCLAIMER = `
298
+ ---
299
+
300
+ ## ⚠️ Scope of this audit
301
+
302
+ This report combines:
303
+ 1. **Automated static scan** - regex/pattern heuristics flagging common antipatterns (tx.origin, reentrancy patterns, unchecked low-level calls, etc.)
304
+ 2. **LLM review over those findings + the contract source** - model-generated analysis, not a formal proof
305
+
306
+ This is **NOT** a substitute for:
307
+ - A professional security audit (CertiK, Trail of Bits, OpenZeppelin, Spearbit, etc.)
308
+ - Formal verification (Certora, Halmos)
309
+ - Specialized tooling (Slither, Mythril, Echidna, Manticore)
310
+ - Manual review by an experienced Solidity engineer
311
+
312
+ **Do not deploy contracts holding user funds based on this report alone.** Treat findings as a starting point for human review.
313
+ `;