solidity-argus 0.1.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.
Files changed (131) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/package.json +43 -0
  5. package/skills/INVENTORY.md +79 -0
  6. package/skills/README.md +56 -0
  7. package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
  8. package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
  9. package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
  10. package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
  11. package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
  12. package/skills/checklists/general-audit/SKILL.md +433 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +129 -0
  14. package/skills/methodology/report-template/SKILL.md +190 -0
  15. package/skills/methodology/severity-classification/SKILL.md +179 -0
  16. package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
  17. package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
  18. package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
  19. package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
  20. package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
  21. package/skills/references/exploit-reference/SKILL.md +259 -0
  22. package/skills/references/smartbugs-examples/SKILL.md +296 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
  28. package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
  29. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
  30. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
  31. package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
  32. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
  33. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
  34. package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
  35. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
  36. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
  37. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
  38. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
  39. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
  40. package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
  41. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
  42. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
  43. package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
  44. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
  45. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
  46. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
  47. package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
  48. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
  49. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
  50. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
  51. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
  52. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
  53. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
  54. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
  55. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
  56. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
  57. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
  58. package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
  59. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
  60. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
  61. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
  62. package/src/agents/argus-prompt.ts +407 -0
  63. package/src/agents/pythia-prompt.ts +134 -0
  64. package/src/agents/scribe-prompt.ts +87 -0
  65. package/src/agents/sentinel-prompt.ts +133 -0
  66. package/src/cli/cli-program.ts +67 -0
  67. package/src/cli/commands/doctor.ts +83 -0
  68. package/src/cli/commands/init.ts +46 -0
  69. package/src/cli/commands/install.ts +55 -0
  70. package/src/cli/index.ts +13 -0
  71. package/src/cli/tui-prompts.ts +75 -0
  72. package/src/cli/types.ts +9 -0
  73. package/src/config/index.ts +3 -0
  74. package/src/config/loader.ts +36 -0
  75. package/src/config/schema.ts +82 -0
  76. package/src/config/types.ts +4 -0
  77. package/src/constants/defaults.ts +6 -0
  78. package/src/create-hooks.ts +84 -0
  79. package/src/create-managers.ts +26 -0
  80. package/src/create-tools.ts +30 -0
  81. package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
  82. package/src/features/audit-enforcer/index.ts +1 -0
  83. package/src/features/background-agent/background-manager.ts +200 -0
  84. package/src/features/background-agent/index.ts +1 -0
  85. package/src/features/context-monitor/context-monitor.ts +48 -0
  86. package/src/features/context-monitor/index.ts +4 -0
  87. package/src/features/context-monitor/tool-output-truncator.ts +17 -0
  88. package/src/features/error-recovery/index.ts +2 -0
  89. package/src/features/error-recovery/session-recovery.ts +27 -0
  90. package/src/features/error-recovery/tool-error-recovery.ts +35 -0
  91. package/src/features/index.ts +5 -0
  92. package/src/features/persistent-state/audit-state-manager.ts +121 -0
  93. package/src/features/persistent-state/index.ts +1 -0
  94. package/src/hooks/compaction-hook.ts +50 -0
  95. package/src/hooks/config-handler.ts +116 -0
  96. package/src/hooks/event-hook-v2.ts +93 -0
  97. package/src/hooks/event-hook.ts +74 -0
  98. package/src/hooks/hook-system.ts +9 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/knowledge-sync-hook.ts +57 -0
  101. package/src/hooks/safe-create-hook.ts +15 -0
  102. package/src/hooks/system-prompt-hook.ts +126 -0
  103. package/src/hooks/tool-tracking-hook.ts +234 -0
  104. package/src/hooks/types.ts +16 -0
  105. package/src/index.ts +36 -0
  106. package/src/knowledge/scvd-client.ts +242 -0
  107. package/src/knowledge/scvd-index.ts +183 -0
  108. package/src/knowledge/scvd-sync.ts +85 -0
  109. package/src/managers/index.ts +1 -0
  110. package/src/managers/types.ts +85 -0
  111. package/src/plugin-interface.ts +38 -0
  112. package/src/shared/binary-utils.ts +63 -0
  113. package/src/shared/deep-merge.ts +71 -0
  114. package/src/shared/file-utils.ts +56 -0
  115. package/src/shared/index.ts +5 -0
  116. package/src/shared/jsonc-parser.ts +39 -0
  117. package/src/shared/logger.ts +36 -0
  118. package/src/state/audit-state.ts +27 -0
  119. package/src/state/finding-store.ts +126 -0
  120. package/src/state/plugin-state.ts +14 -0
  121. package/src/state/types.ts +61 -0
  122. package/src/tools/contract-analyzer-tool.ts +184 -0
  123. package/src/tools/forge-fuzz-tool.ts +311 -0
  124. package/src/tools/forge-test-tool.ts +397 -0
  125. package/src/tools/pattern-checker-tool.ts +337 -0
  126. package/src/tools/report-generator-tool.ts +308 -0
  127. package/src/tools/slither-tool.ts +465 -0
  128. package/src/tools/solodit-search-tool.ts +131 -0
  129. package/src/tools/sync-knowledge-tool.ts +116 -0
  130. package/src/utils/project-detector.ts +133 -0
  131. package/src/utils/solidity-parser.ts +174 -0
@@ -0,0 +1,397 @@
1
+ import { tool, type ToolContext } from "@opencode-ai/plugin";
2
+
3
+ type ForgeTestArgs = {
4
+ target?: string;
5
+ match_test?: string;
6
+ match_contract?: string;
7
+ fork_url?: string;
8
+ verbosity?: number;
9
+ gas_report?: boolean;
10
+ coverage?: boolean;
11
+ };
12
+
13
+ type NormalizedForgeTestArgs = {
14
+ target: string;
15
+ match_test?: string;
16
+ match_contract?: string;
17
+ fork_url?: string;
18
+ verbosity: number;
19
+ gas_report?: boolean;
20
+ coverage: boolean;
21
+ };
22
+
23
+ type ForgeTestItem = {
24
+ name: string;
25
+ contract: string;
26
+ status: "pass" | "fail";
27
+ gas: number;
28
+ };
29
+
30
+ type ForgeTestSummary = {
31
+ passed: number;
32
+ failed: number;
33
+ skipped: number;
34
+ total: number;
35
+ };
36
+
37
+ type ForgeCoverageFile = {
38
+ path: string;
39
+ lines: number;
40
+ branches: number;
41
+ functions: number;
42
+ uncoveredFunctions: string[];
43
+ };
44
+
45
+ type ForgeTestResult = {
46
+ success: boolean;
47
+ summary: ForgeTestSummary;
48
+ tests: ForgeTestItem[];
49
+ gasReport?: Record<string, unknown>;
50
+ coverageReport?: { files: ForgeCoverageFile[] };
51
+ executionTime: number;
52
+ error?: string;
53
+ };
54
+
55
+ export type ForgeCommandResult = {
56
+ stdout: string;
57
+ stderr: string;
58
+ exitCode: number;
59
+ };
60
+
61
+ type RunForgeCommand = (
62
+ command: string[],
63
+ signal: AbortSignal,
64
+ cwd: string
65
+ ) => Promise<ForgeCommandResult>;
66
+
67
+ type ForgeTestPayload = {
68
+ success?: boolean;
69
+ tests?:
70
+ | Record<string, Record<string, { status?: string; gas?: number }>>
71
+ | Array<{ name?: string; contract?: string; status?: string; gas?: number }>;
72
+ summary?: {
73
+ passed?: number;
74
+ failed?: number;
75
+ skipped?: number;
76
+ total?: number;
77
+ };
78
+ gas_report?: Record<string, unknown>;
79
+ gasReport?: Record<string, unknown>;
80
+ };
81
+
82
+ type CoveragePayload = {
83
+ files?: Array<Record<string, unknown>>;
84
+ coverage?: Record<string, Record<string, unknown>>;
85
+ };
86
+
87
+ function mapStatus(input?: string): "pass" | "fail" | "skip" {
88
+ const normalized = (input ?? "").toLowerCase();
89
+ if (normalized.includes("skip") || normalized.includes("ignore")) {
90
+ return "skip";
91
+ }
92
+ if (normalized.includes("pass") || normalized.includes("success")) {
93
+ return "pass";
94
+ }
95
+ return "fail";
96
+ }
97
+
98
+ function toNumber(input: unknown, fallback = 0): number {
99
+ return typeof input === "number" && Number.isFinite(input) ? input : fallback;
100
+ }
101
+
102
+ function parseTests(payload: ForgeTestPayload): {
103
+ tests: ForgeTestItem[];
104
+ summary: ForgeTestSummary;
105
+ } {
106
+ const collected: Array<ForgeTestItem | { skipped: true }> = [];
107
+
108
+ if (Array.isArray(payload.tests)) {
109
+ for (const item of payload.tests) {
110
+ const status = mapStatus(item.status);
111
+ if (status === "skip") {
112
+ collected.push({ skipped: true });
113
+ continue;
114
+ }
115
+ collected.push({
116
+ name: item.name ?? "unknown-test",
117
+ contract: item.contract ?? "unknown-contract",
118
+ status,
119
+ gas: toNumber(item.gas),
120
+ });
121
+ }
122
+ } else if (payload.tests && typeof payload.tests === "object") {
123
+ const entries = Object.entries(payload.tests);
124
+ for (const [contract, tests] of entries) {
125
+ for (const [name, details] of Object.entries(tests)) {
126
+ const status = mapStatus(details.status);
127
+ if (status === "skip") {
128
+ collected.push({ skipped: true });
129
+ continue;
130
+ }
131
+ collected.push({
132
+ name,
133
+ contract,
134
+ status,
135
+ gas: toNumber(details.gas),
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ const tests = collected.filter((item): item is ForgeTestItem => !("skipped" in item));
142
+ const passed = tests.filter((item) => item.status === "pass").length;
143
+ const failed = tests.filter((item) => item.status === "fail").length;
144
+ const skippedFromTests = collected.length - tests.length;
145
+ const summary = payload.summary;
146
+ const skipped =
147
+ typeof summary?.skipped === "number" ? summary.skipped : skippedFromTests;
148
+ const total =
149
+ typeof summary?.total === "number" ? summary.total : passed + failed + skipped;
150
+
151
+ return {
152
+ tests,
153
+ summary: {
154
+ passed: typeof summary?.passed === "number" ? summary.passed : passed,
155
+ failed: typeof summary?.failed === "number" ? summary.failed : failed,
156
+ skipped,
157
+ total,
158
+ },
159
+ };
160
+ }
161
+
162
+ function valueFromRecord(record: Record<string, unknown>, keys: string[]): unknown {
163
+ for (const key of keys) {
164
+ if (key in record) {
165
+ return record[key];
166
+ }
167
+ }
168
+ return undefined;
169
+ }
170
+
171
+ function parseUncoveredFunctions(input: unknown): string[] {
172
+ if (!Array.isArray(input)) {
173
+ return [];
174
+ }
175
+
176
+ return input
177
+ .map((value) => {
178
+ if (typeof value === "string") {
179
+ return value;
180
+ }
181
+ if (value && typeof value === "object" && "name" in value) {
182
+ const name = (value as { name?: unknown }).name;
183
+ return typeof name === "string" ? name : "";
184
+ }
185
+ return "";
186
+ })
187
+ .filter((value) => value.length > 0);
188
+ }
189
+
190
+ function normalizeCoverageFile(file: Record<string, unknown>): ForgeCoverageFile {
191
+ return {
192
+ path: (valueFromRecord(file, ["path", "file", "name"]) as string) ?? "unknown",
193
+ lines: toNumber(valueFromRecord(file, ["lines", "lineCoverage", "line_coverage"])),
194
+ branches: toNumber(
195
+ valueFromRecord(file, ["branches", "branchCoverage", "branch_coverage"])
196
+ ),
197
+ functions: toNumber(
198
+ valueFromRecord(file, ["functions", "functionCoverage", "function_coverage"])
199
+ ),
200
+ uncoveredFunctions: parseUncoveredFunctions(
201
+ valueFromRecord(file, ["uncoveredFunctions", "uncovered_functions"])
202
+ ),
203
+ };
204
+ }
205
+
206
+ function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] } {
207
+ if (Array.isArray(payload.files)) {
208
+ return {
209
+ files: payload.files
210
+ .filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
211
+ .map((item) => normalizeCoverageFile(item)),
212
+ };
213
+ }
214
+
215
+ if (payload.coverage && typeof payload.coverage === "object") {
216
+ const files: ForgeCoverageFile[] = [];
217
+ for (const [path, metrics] of Object.entries(payload.coverage)) {
218
+ if (!metrics || typeof metrics !== "object") {
219
+ continue;
220
+ }
221
+ files.push(
222
+ normalizeCoverageFile({
223
+ path,
224
+ ...metrics,
225
+ })
226
+ );
227
+ }
228
+
229
+ return { files };
230
+ }
231
+
232
+ return { files: [] };
233
+ }
234
+
235
+ function normalizeArgs(args: ForgeTestArgs): NormalizedForgeTestArgs {
236
+ return {
237
+ target: args.target ?? ".",
238
+ match_test: args.match_test,
239
+ match_contract: args.match_contract,
240
+ fork_url: args.fork_url,
241
+ verbosity:
242
+ typeof args.verbosity === "number" && args.verbosity >= 1 && args.verbosity <= 5
243
+ ? args.verbosity
244
+ : 3,
245
+ gas_report: args.gas_report,
246
+ coverage: args.coverage ?? false,
247
+ };
248
+ }
249
+
250
+ function buildForgeTestCommand(args: NormalizedForgeTestArgs): string[] {
251
+ const command = ["forge", "test", "--json", `-v${"v".repeat(args.verbosity - 1)}`];
252
+
253
+ if (args.match_test) {
254
+ command.push("--match-test", args.match_test);
255
+ }
256
+ if (args.match_contract) {
257
+ command.push("--match-contract", args.match_contract);
258
+ }
259
+ if (args.fork_url) {
260
+ command.push("--fork-url", args.fork_url);
261
+ }
262
+ if (args.gas_report) {
263
+ command.push("--gas-report");
264
+ }
265
+
266
+ return command;
267
+ }
268
+
269
+ const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
270
+ const child = Bun.spawn(command, {
271
+ cwd,
272
+ stdout: "pipe",
273
+ stderr: "pipe",
274
+ signal,
275
+ });
276
+
277
+ const [exitCode, stdout, stderr] = await Promise.all([
278
+ child.exited,
279
+ new Response(child.stdout).text(),
280
+ new Response(child.stderr).text(),
281
+ ]);
282
+
283
+ return {
284
+ stdout,
285
+ stderr,
286
+ exitCode,
287
+ };
288
+ };
289
+
290
+ export async function executeForgeTest(
291
+ args: ForgeTestArgs,
292
+ context: ToolContext,
293
+ runCommand: RunForgeCommand = runForgeCommand
294
+ ): Promise<ForgeTestResult> {
295
+ const startedAt = Date.now();
296
+ const normalizedArgs = normalizeArgs(args);
297
+ context.metadata({ title: `Run forge test: ${normalizedArgs.target}` });
298
+
299
+ const fail = (error: string): ForgeTestResult => ({
300
+ success: false,
301
+ summary: { passed: 0, failed: 0, skipped: 0, total: 0 },
302
+ tests: [],
303
+ executionTime: Date.now() - startedAt,
304
+ error,
305
+ });
306
+
307
+ try {
308
+ const testResult = await runCommand(
309
+ buildForgeTestCommand(normalizedArgs),
310
+ context.abort,
311
+ normalizedArgs.target
312
+ );
313
+
314
+ let payload: ForgeTestPayload;
315
+ try {
316
+ payload = JSON.parse(testResult.stdout) as ForgeTestPayload;
317
+ } catch {
318
+ return fail("Invalid JSON output from forge test");
319
+ }
320
+
321
+ const parsed = parseTests(payload);
322
+ const output: ForgeTestResult = {
323
+ success:
324
+ testResult.exitCode === 0 &&
325
+ parsed.summary.failed === 0 &&
326
+ (payload.success ?? true) === true,
327
+ summary: parsed.summary,
328
+ tests: parsed.tests,
329
+ executionTime: Date.now() - startedAt,
330
+ };
331
+
332
+ const gasReport = payload.gas_report ?? payload.gasReport;
333
+ if (gasReport) {
334
+ output.gasReport = gasReport;
335
+ }
336
+
337
+ if (normalizedArgs.coverage) {
338
+ const coverageResult = await runCommand(
339
+ ["forge", "coverage", "--report", "json"],
340
+ context.abort,
341
+ normalizedArgs.target
342
+ );
343
+ if (coverageResult.exitCode !== 0) {
344
+ output.error = coverageResult.stderr.trim() || "forge coverage failed";
345
+ output.success = false;
346
+ } else {
347
+ try {
348
+ const coveragePayload = JSON.parse(coverageResult.stdout) as CoveragePayload;
349
+ output.coverageReport = parseCoverage(coveragePayload);
350
+ } catch {
351
+ output.error = "Invalid JSON output from forge coverage";
352
+ output.success = false;
353
+ }
354
+ }
355
+ }
356
+
357
+ if (testResult.exitCode !== 0 && !output.error) {
358
+ output.error = testResult.stderr.trim() || `forge test exited with code ${testResult.exitCode}`;
359
+ }
360
+
361
+ return output;
362
+ } catch (error) {
363
+ if (context.abort.aborted || (error instanceof DOMException && error.name === "AbortError")) {
364
+ return fail("forge test aborted");
365
+ }
366
+
367
+ const maybeError = error as Error & { code?: string };
368
+ if (maybeError.code === "ENOENT") {
369
+ return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash");
370
+ }
371
+ if (
372
+ maybeError.code === "ETIMEDOUT" ||
373
+ maybeError.message.toLowerCase().includes("timed out")
374
+ ) {
375
+ return fail("forge test timed out");
376
+ }
377
+
378
+ return fail(maybeError.message || "forge test failed");
379
+ }
380
+ }
381
+
382
+ export const forgeTestTool = tool({
383
+ description: "Run forge test with optional coverage and return normalized results.",
384
+ args: {
385
+ target: tool.schema.string().default("."),
386
+ match_test: tool.schema.string().optional(),
387
+ match_contract: tool.schema.string().optional(),
388
+ fork_url: tool.schema.string().optional(),
389
+ verbosity: tool.schema.number().min(1).max(5).default(3),
390
+ gas_report: tool.schema.boolean().optional(),
391
+ coverage: tool.schema.boolean().default(false),
392
+ },
393
+ async execute(args, context) {
394
+ const result = await executeForgeTest(args, context);
395
+ return JSON.stringify(result);
396
+ },
397
+ });