solidity-argus 0.1.8 → 0.3.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/AGENTS.md +3 -3
- package/README.md +229 -13
- package/package.json +37 -8
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +72 -6
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +9 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +9 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +27 -10
- package/src/agents/pythia-prompt.ts +7 -8
- package/src/agents/scribe-prompt.ts +10 -5
- package/src/agents/sentinel-prompt.ts +36 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +29 -22
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +303 -23
- package/src/cli/commands/init.ts +8 -6
- package/src/cli/commands/install.ts +10 -8
- package/src/cli/commands/lint-skills.ts +118 -0
- package/src/cli/index.ts +5 -5
- package/src/cli/tui-prompts.ts +4 -2
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +225 -29
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +14 -8
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +79 -19
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +158 -52
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +31 -11
- package/src/hooks/context-budget.ts +42 -0
- package/src/hooks/event-hook.ts +48 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +19 -21
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +9 -11
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +162 -29
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -13
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +103 -83
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +110 -62
- package/src/knowledge/scvd-sync.ts +223 -47
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +19 -8
- package/src/shared/binary-utils.ts +44 -34
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +91 -17
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +237 -0
- package/src/skills/skill-schema.ts +99 -0
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +10 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +96 -44
- package/src/tools/argus-skill-load-tool.ts +78 -0
- package/src/tools/contract-analyzer-tool.ts +60 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +206 -167
- package/src/tools/pattern-loader.ts +77 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +333 -142
- package/src/tools/slither-tool.ts +300 -210
- package/src/tools/solodit-search-tool.ts +255 -80
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +118 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +175 -86
- package/src/utils/solidity-parser.ts +112 -67
- package/src/utils/solodit-health.ts +29 -0
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { existsSync } from "fs"
|
|
2
|
-
import { join, resolve } from "path"
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { join, resolve } from "node:path"
|
|
3
|
+
import { type DependencyRisk, scanDependencyRisks } from "./dependency-scanner"
|
|
3
4
|
|
|
4
5
|
export interface ProjectConfig {
|
|
5
|
-
type: "foundry" | "hardhat" | "mixed" | "unknown"
|
|
6
|
-
srcDir: string
|
|
7
|
-
testDir: string
|
|
8
|
-
solcVersion?: string
|
|
9
|
-
remappings: string[]
|
|
10
|
-
viaIr: boolean
|
|
11
|
-
rootDir: string
|
|
6
|
+
type: "foundry" | "hardhat" | "mixed" | "unknown"
|
|
7
|
+
srcDir: string
|
|
8
|
+
testDir: string
|
|
9
|
+
solcVersion?: string
|
|
10
|
+
remappings: string[]
|
|
11
|
+
viaIr: boolean
|
|
12
|
+
rootDir: string
|
|
13
|
+
optimizer?: { enabled: boolean; runs?: number }
|
|
14
|
+
evmVersion?: string
|
|
15
|
+
profiles?: string[]
|
|
16
|
+
hasHardhat: boolean
|
|
17
|
+
hasFoundry: boolean
|
|
18
|
+
dependencies?: Record<string, string>
|
|
19
|
+
devDependencies?: Record<string, string>
|
|
20
|
+
isUpgradeable: boolean
|
|
21
|
+
outDir?: string
|
|
22
|
+
dependencyRisks: DependencyRisk[]
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -17,50 +28,64 @@ export interface ProjectConfig {
|
|
|
17
28
|
* @returns ProjectConfig with detected framework type and settings
|
|
18
29
|
*/
|
|
19
30
|
export async function detectProject(dir: string): Promise<ProjectConfig> {
|
|
20
|
-
const rootDir = resolve(dir)
|
|
21
|
-
const foundryTomlPath = join(rootDir, "foundry.toml")
|
|
22
|
-
const hardhatConfigTsPath = join(rootDir, "hardhat.config.ts")
|
|
23
|
-
const hardhatConfigJsPath = join(rootDir, "hardhat.config.js")
|
|
31
|
+
const rootDir = resolve(dir)
|
|
32
|
+
const foundryTomlPath = join(rootDir, "foundry.toml")
|
|
33
|
+
const hardhatConfigTsPath = join(rootDir, "hardhat.config.ts")
|
|
34
|
+
const hardhatConfigJsPath = join(rootDir, "hardhat.config.js")
|
|
24
35
|
|
|
25
|
-
const hasFoundry = existsSync(foundryTomlPath)
|
|
26
|
-
const hasHardhatTs = existsSync(hardhatConfigTsPath)
|
|
27
|
-
const hasHardhatJs = existsSync(hardhatConfigJsPath)
|
|
28
|
-
const hasHardhat = hasHardhatTs || hasHardhatJs
|
|
36
|
+
const hasFoundry = existsSync(foundryTomlPath)
|
|
37
|
+
const hasHardhatTs = existsSync(hardhatConfigTsPath)
|
|
38
|
+
const hasHardhatJs = existsSync(hardhatConfigJsPath)
|
|
39
|
+
const hasHardhat = hasHardhatTs || hasHardhatJs
|
|
29
40
|
|
|
30
41
|
// Determine project type
|
|
31
|
-
let type: "foundry" | "hardhat" | "mixed" | "unknown"
|
|
42
|
+
let type: "foundry" | "hardhat" | "mixed" | "unknown"
|
|
32
43
|
if (hasFoundry && hasHardhat) {
|
|
33
|
-
type = "mixed"
|
|
44
|
+
type = "mixed"
|
|
34
45
|
} else if (hasFoundry) {
|
|
35
|
-
type = "foundry"
|
|
46
|
+
type = "foundry"
|
|
36
47
|
} else if (hasHardhat) {
|
|
37
|
-
type = "hardhat"
|
|
48
|
+
type = "hardhat"
|
|
38
49
|
} else {
|
|
39
|
-
type = "unknown"
|
|
50
|
+
type = "unknown"
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
let
|
|
45
|
-
let
|
|
46
|
-
let
|
|
47
|
-
let
|
|
53
|
+
let srcDir = "src"
|
|
54
|
+
let testDir = "test"
|
|
55
|
+
let solcVersion: string | undefined
|
|
56
|
+
let remappings: string[] = []
|
|
57
|
+
let viaIr = false
|
|
58
|
+
let optimizer: { enabled: boolean; runs?: number } | undefined
|
|
59
|
+
let evmVersion: string | undefined
|
|
60
|
+
let profiles: string[] | undefined
|
|
61
|
+
let outDir: string | undefined
|
|
48
62
|
|
|
49
|
-
// Parse Foundry config if present
|
|
50
63
|
if (hasFoundry) {
|
|
51
|
-
const foundryConfig = await parseFoundryToml(foundryTomlPath)
|
|
52
|
-
srcDir = foundryConfig.srcDir || srcDir
|
|
53
|
-
testDir = foundryConfig.testDir || testDir
|
|
54
|
-
solcVersion = foundryConfig.solcVersion
|
|
55
|
-
remappings = foundryConfig.remappings
|
|
56
|
-
viaIr = foundryConfig.viaIr
|
|
64
|
+
const foundryConfig = await parseFoundryToml(foundryTomlPath)
|
|
65
|
+
srcDir = foundryConfig.srcDir || srcDir
|
|
66
|
+
testDir = foundryConfig.testDir || testDir
|
|
67
|
+
solcVersion = foundryConfig.solcVersion
|
|
68
|
+
remappings = foundryConfig.remappings
|
|
69
|
+
viaIr = foundryConfig.viaIr
|
|
70
|
+
optimizer = foundryConfig.optimizer
|
|
71
|
+
evmVersion = foundryConfig.evmVersion
|
|
72
|
+
profiles = foundryConfig.profiles
|
|
73
|
+
outDir = foundryConfig.outDir
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const remappingsFromTxt = parseRemappingsTxt(rootDir)
|
|
77
|
+
if (remappingsFromTxt.length > 0 && remappings.length === 0) {
|
|
78
|
+
remappings = remappingsFromTxt
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
// Set Hardhat defaults if it's a Hardhat project
|
|
60
81
|
if (hasHardhat && !hasFoundry) {
|
|
61
|
-
srcDir = "contracts"
|
|
82
|
+
srcDir = "contracts"
|
|
62
83
|
}
|
|
63
84
|
|
|
85
|
+
const isUpgradeable = existsSync(join(rootDir, ".openzeppelin"))
|
|
86
|
+
|
|
87
|
+
const { dependencies, devDependencies } = await parsePackageJson(rootDir)
|
|
88
|
+
|
|
64
89
|
return {
|
|
65
90
|
type,
|
|
66
91
|
srcDir,
|
|
@@ -69,77 +94,141 @@ export async function detectProject(dir: string): Promise<ProjectConfig> {
|
|
|
69
94
|
remappings,
|
|
70
95
|
viaIr,
|
|
71
96
|
rootDir,
|
|
72
|
-
|
|
97
|
+
optimizer,
|
|
98
|
+
evmVersion,
|
|
99
|
+
profiles,
|
|
100
|
+
hasHardhat,
|
|
101
|
+
hasFoundry,
|
|
102
|
+
dependencies,
|
|
103
|
+
devDependencies,
|
|
104
|
+
isUpgradeable,
|
|
105
|
+
outDir,
|
|
106
|
+
dependencyRisks: scanDependencyRisks({ dependencies, devDependencies }),
|
|
107
|
+
}
|
|
73
108
|
}
|
|
74
109
|
|
|
75
110
|
/**
|
|
76
111
|
* Parses foundry.toml file using regex-based parsing
|
|
77
112
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
interface FoundryTomlResult {
|
|
114
|
+
srcDir?: string
|
|
115
|
+
testDir?: string
|
|
116
|
+
solcVersion?: string
|
|
117
|
+
remappings: string[]
|
|
118
|
+
viaIr: boolean
|
|
119
|
+
optimizer?: { enabled: boolean; runs?: number }
|
|
120
|
+
evmVersion?: string
|
|
121
|
+
profiles?: string[]
|
|
122
|
+
outDir?: string
|
|
123
|
+
}
|
|
88
124
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
async function parseFoundryToml(filePath: string): Promise<FoundryTomlResult> {
|
|
126
|
+
const content = await Bun.file(filePath).text()
|
|
127
|
+
|
|
128
|
+
const result: FoundryTomlResult = {
|
|
129
|
+
srcDir: undefined,
|
|
130
|
+
testDir: undefined,
|
|
131
|
+
solcVersion: undefined,
|
|
132
|
+
remappings: [],
|
|
94
133
|
viaIr: false,
|
|
95
|
-
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const profileNames = Array.from(content.matchAll(/\[profile\.(\w+)\]/g), (m) => m[1]).filter(
|
|
137
|
+
(name): name is string => Boolean(name),
|
|
138
|
+
)
|
|
139
|
+
if (profileNames.length > 0) {
|
|
140
|
+
result.profiles = profileNames
|
|
141
|
+
}
|
|
96
142
|
|
|
97
|
-
|
|
98
|
-
const profileDefaultMatch = content.match(
|
|
99
|
-
/\[profile\.default\]([\s\S]*?)(?:\n\[|$)/
|
|
100
|
-
);
|
|
143
|
+
const profileDefaultMatch = content.match(/\[profile\.default\]([\s\S]*?)(?:\n\[|$)/)
|
|
101
144
|
if (!profileDefaultMatch || !profileDefaultMatch[1]) {
|
|
102
|
-
return result
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const profileSection = profileDefaultMatch[1]
|
|
149
|
+
|
|
150
|
+
const srcMatch = profileSection.match(/^\s*src\s*=\s*["']([^"']+)["']/m)
|
|
151
|
+
if (srcMatch?.[1]) {
|
|
152
|
+
result.srcDir = srcMatch[1]
|
|
103
153
|
}
|
|
104
154
|
|
|
105
|
-
const
|
|
155
|
+
const testMatch = profileSection.match(/^\s*test\s*=\s*["']([^"']+)["']/m)
|
|
156
|
+
if (testMatch?.[1]) {
|
|
157
|
+
result.testDir = testMatch[1]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const solcMatch = profileSection.match(/^\s*solc\s*=\s*["']([^"']+)["']/m)
|
|
161
|
+
if (solcMatch?.[1]) {
|
|
162
|
+
result.solcVersion = solcMatch[1]
|
|
163
|
+
}
|
|
106
164
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
result.srcDir = srcMatch[1];
|
|
165
|
+
const viaIrMatch = profileSection.match(/^\s*via[_-]ir\s*=\s*(true|false)/m)
|
|
166
|
+
if (viaIrMatch?.[1] === "true") {
|
|
167
|
+
result.viaIr = true
|
|
111
168
|
}
|
|
112
169
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
170
|
+
const optimizerMatch = profileSection.match(/^\s*optimizer\s*=\s*(true|false)/m)
|
|
171
|
+
if (optimizerMatch?.[1]) {
|
|
172
|
+
const enabled = optimizerMatch[1] === "true"
|
|
173
|
+
const runsMatch = profileSection.match(/^\s*optimizer_runs\s*=\s*(\d+)/m)
|
|
174
|
+
result.optimizer = {
|
|
175
|
+
enabled,
|
|
176
|
+
runs: runsMatch?.[1] ? parseInt(runsMatch[1], 10) : undefined,
|
|
177
|
+
}
|
|
117
178
|
}
|
|
118
179
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
result.solcVersion = solcMatch[1];
|
|
180
|
+
const evmMatch = profileSection.match(/^\s*evm_version\s*=\s*["']([^"']+)["']/m)
|
|
181
|
+
if (evmMatch?.[1]) {
|
|
182
|
+
result.evmVersion = evmMatch[1]
|
|
123
183
|
}
|
|
124
184
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
result.viaIr = true;
|
|
185
|
+
const outMatch = profileSection.match(/^\s*out\s*=\s*["']([^"']+)["']/m)
|
|
186
|
+
if (outMatch?.[1]) {
|
|
187
|
+
result.outDir = outMatch[1]
|
|
129
188
|
}
|
|
130
189
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/
|
|
134
|
-
);
|
|
135
|
-
if (remappingsMatch && remappingsMatch[1]) {
|
|
136
|
-
const remappingsContent = remappingsMatch[1];
|
|
137
|
-
// Extract quoted strings from the array
|
|
138
|
-
const remappingMatches = remappingsContent.match(/["']([^"']+)["']/g);
|
|
190
|
+
const remappingsMatch = profileSection.match(/remappings\s*=\s*\[([\s\S]*?)\]/)
|
|
191
|
+
if (remappingsMatch?.[1]) {
|
|
192
|
+
const remappingMatches = remappingsMatch[1].match(/["']([^"']+)["']/g)
|
|
139
193
|
if (remappingMatches) {
|
|
140
|
-
result.remappings = remappingMatches.map((m) => m.slice(1, -1))
|
|
194
|
+
result.remappings = remappingMatches.map((m) => m.slice(1, -1))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function parsePackageJson(rootDir: string): Promise<{
|
|
202
|
+
dependencies?: Record<string, string>
|
|
203
|
+
devDependencies?: Record<string, string>
|
|
204
|
+
}> {
|
|
205
|
+
const pkgPath = join(rootDir, "package.json")
|
|
206
|
+
if (!existsSync(pkgPath)) {
|
|
207
|
+
return {}
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const content = JSON.parse(await Bun.file(pkgPath).text())
|
|
211
|
+
return {
|
|
212
|
+
dependencies: content.dependencies,
|
|
213
|
+
devDependencies: content.devDependencies,
|
|
141
214
|
}
|
|
215
|
+
} catch {
|
|
216
|
+
return {}
|
|
142
217
|
}
|
|
218
|
+
}
|
|
143
219
|
|
|
144
|
-
|
|
220
|
+
function parseRemappingsTxt(rootDir: string): string[] {
|
|
221
|
+
const remappingsPath = join(rootDir, "remappings.txt")
|
|
222
|
+
if (!existsSync(remappingsPath)) {
|
|
223
|
+
return []
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const content = require("node:fs").readFileSync(remappingsPath, "utf-8")
|
|
227
|
+
return content
|
|
228
|
+
.split("\n")
|
|
229
|
+
.map((line: string) => line.trim())
|
|
230
|
+
.filter((line: string) => line.length > 0)
|
|
231
|
+
} catch {
|
|
232
|
+
return []
|
|
233
|
+
}
|
|
145
234
|
}
|
|
@@ -1,22 +1,72 @@
|
|
|
1
|
-
import type { ContractProfile } from "../state/types"
|
|
1
|
+
import type { ContractProfile } from "../state/types"
|
|
2
2
|
|
|
3
3
|
interface ABIFunction {
|
|
4
|
-
type: string
|
|
5
|
-
name: string
|
|
6
|
-
inputs?: Array<{ name: string; type: string }
|
|
7
|
-
outputs?: Array<{ name: string; type: string }
|
|
8
|
-
stateMutability?: string
|
|
4
|
+
type: string
|
|
5
|
+
name: string
|
|
6
|
+
inputs?: Array<{ name: string; type: string }>
|
|
7
|
+
outputs?: Array<{ name: string; type: string }>
|
|
8
|
+
stateMutability?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
interface StorageLayoutItem {
|
|
12
|
-
label: string
|
|
13
|
-
type: string
|
|
14
|
-
slot: string
|
|
12
|
+
label: string
|
|
13
|
+
type: string
|
|
14
|
+
slot: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
interface StorageLayout {
|
|
18
|
-
storage: StorageLayoutItem[]
|
|
19
|
-
types: Record<string, { label: string }
|
|
18
|
+
storage: StorageLayoutItem[]
|
|
19
|
+
types: Record<string, { label: string }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract the first JSON value from a string that may contain non-JSON
|
|
24
|
+
* prefix (e.g. forge table-format output, compilation progress).
|
|
25
|
+
* Falls back to the original string if no JSON delimiter is found.
|
|
26
|
+
*/
|
|
27
|
+
function extractJson(raw: string, opener: "[" | "{"): string {
|
|
28
|
+
const _closer = opener === "[" ? "]" : "}"
|
|
29
|
+
const start = raw.indexOf(opener)
|
|
30
|
+
if (start === -1) return raw
|
|
31
|
+
|
|
32
|
+
let depth = 0
|
|
33
|
+
let inString = false
|
|
34
|
+
let escaped = false
|
|
35
|
+
|
|
36
|
+
for (let i = start; i < raw.length; i++) {
|
|
37
|
+
const ch = raw.charAt(i)
|
|
38
|
+
|
|
39
|
+
if (inString) {
|
|
40
|
+
if (escaped) {
|
|
41
|
+
escaped = false
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if (ch === "\\") {
|
|
45
|
+
escaped = true
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (ch === '"') {
|
|
49
|
+
inString = false
|
|
50
|
+
}
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (ch === '"') {
|
|
55
|
+
inString = true
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (ch === "{" || ch === "[") {
|
|
60
|
+
depth++
|
|
61
|
+
} else if (ch === "}" || ch === "]") {
|
|
62
|
+
depth--
|
|
63
|
+
if (depth === 0) {
|
|
64
|
+
return raw.slice(start, i + 1)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return raw
|
|
20
70
|
}
|
|
21
71
|
|
|
22
72
|
/**
|
|
@@ -27,7 +77,7 @@ interface StorageLayout {
|
|
|
27
77
|
*/
|
|
28
78
|
export async function extractContractInfo(
|
|
29
79
|
contractName: string,
|
|
30
|
-
projectDir: string
|
|
80
|
+
projectDir: string,
|
|
31
81
|
): Promise<ContractProfile> {
|
|
32
82
|
const result: ContractProfile = {
|
|
33
83
|
name: contractName,
|
|
@@ -38,89 +88,90 @@ export async function extractContractInfo(
|
|
|
38
88
|
accessControlPattern: "none",
|
|
39
89
|
externalCalls: [],
|
|
40
90
|
riskIndicators: [],
|
|
41
|
-
}
|
|
91
|
+
}
|
|
42
92
|
|
|
43
93
|
try {
|
|
44
94
|
// Run forge inspect abi
|
|
45
|
-
const abiResult = Bun.spawnSync(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
);
|
|
95
|
+
const abiResult = Bun.spawnSync(["forge", "inspect", contractName, "abi", "--json"], {
|
|
96
|
+
cwd: projectDir,
|
|
97
|
+
stdout: "pipe",
|
|
98
|
+
stderr: "pipe",
|
|
99
|
+
timeout: 15_000,
|
|
100
|
+
})
|
|
53
101
|
|
|
54
102
|
if (!abiResult.success) {
|
|
55
|
-
const errorMsg = abiResult.stderr?.toString() || "Unknown error"
|
|
56
|
-
result.error = `Failed to inspect ABI: ${errorMsg}
|
|
57
|
-
return result
|
|
103
|
+
const errorMsg = abiResult.stderr?.toString() || "Unknown error"
|
|
104
|
+
result.error = `Failed to inspect ABI: ${errorMsg}`
|
|
105
|
+
return result
|
|
58
106
|
}
|
|
59
107
|
|
|
60
108
|
// Run forge inspect storage-layout
|
|
61
109
|
const storageResult = Bun.spawnSync(
|
|
62
|
-
["forge", "inspect", contractName, "storage-layout"],
|
|
110
|
+
["forge", "inspect", contractName, "storage-layout", "--json"],
|
|
63
111
|
{
|
|
64
112
|
cwd: projectDir,
|
|
65
113
|
stdout: "pipe",
|
|
66
114
|
stderr: "pipe",
|
|
67
|
-
|
|
68
|
-
|
|
115
|
+
timeout: 15_000,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
69
118
|
|
|
70
119
|
if (!storageResult.success) {
|
|
71
|
-
const errorMsg = storageResult.stderr?.toString() || "Unknown error"
|
|
72
|
-
result.error = `Failed to inspect storage layout: ${errorMsg}
|
|
73
|
-
return result
|
|
120
|
+
const errorMsg = storageResult.stderr?.toString() || "Unknown error"
|
|
121
|
+
result.error = `Failed to inspect storage layout: ${errorMsg}`
|
|
122
|
+
return result
|
|
74
123
|
}
|
|
75
124
|
|
|
76
125
|
// Parse ABI
|
|
77
|
-
const
|
|
78
|
-
|
|
126
|
+
const abiRaw = abiResult.stdout?.toString() || "[]"
|
|
127
|
+
const abiOutput = extractJson(abiRaw, "[")
|
|
128
|
+
let abi: ABIFunction[] = []
|
|
79
129
|
try {
|
|
80
|
-
abi = JSON.parse(abiOutput)
|
|
130
|
+
abi = JSON.parse(abiOutput)
|
|
81
131
|
} catch (e) {
|
|
82
|
-
result.error = `Failed to parse ABI JSON: ${e instanceof Error ? e.message : "Unknown error"}
|
|
83
|
-
return result
|
|
132
|
+
result.error = `Failed to parse ABI JSON: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
133
|
+
return result
|
|
84
134
|
}
|
|
85
135
|
|
|
86
136
|
// Parse storage layout
|
|
87
|
-
const
|
|
88
|
-
|
|
137
|
+
const storageRaw = storageResult.stdout?.toString() || "{}"
|
|
138
|
+
const storageOutput = extractJson(storageRaw, "{")
|
|
139
|
+
let storageLayout: StorageLayout = { storage: [], types: {} }
|
|
89
140
|
try {
|
|
90
|
-
storageLayout = JSON.parse(storageOutput)
|
|
141
|
+
storageLayout = JSON.parse(storageOutput)
|
|
91
142
|
} catch (e) {
|
|
92
|
-
result.error = `Failed to parse storage layout JSON: ${e instanceof Error ? e.message : "Unknown error"}
|
|
93
|
-
return result
|
|
143
|
+
result.error = `Failed to parse storage layout JSON: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
144
|
+
return result
|
|
94
145
|
}
|
|
95
146
|
|
|
96
147
|
// Extract functions from ABI
|
|
97
|
-
const functions = abi.filter((item) => item.type === "function")
|
|
148
|
+
const functions = abi.filter((item) => item.type === "function")
|
|
98
149
|
result.functions = functions.map((func) => ({
|
|
99
150
|
name: func.name || "",
|
|
100
151
|
visibility: mapStateMutabilityToVisibility(func.stateMutability || "nonpayable"),
|
|
101
152
|
mutability: func.stateMutability || "nonpayable",
|
|
102
153
|
modifiers: [],
|
|
103
|
-
}))
|
|
154
|
+
}))
|
|
104
155
|
|
|
105
156
|
// Extract state variables from storage layout
|
|
106
157
|
result.stateVars = storageLayout.storage.map((item) => {
|
|
107
|
-
const typeInfo = storageLayout.types[item.type]
|
|
108
|
-
const typeLabel = typeInfo?.label || item.type
|
|
158
|
+
const typeInfo = storageLayout.types[item.type]
|
|
159
|
+
const typeLabel = typeInfo?.label || item.type
|
|
109
160
|
|
|
110
161
|
return {
|
|
111
162
|
name: item.label,
|
|
112
163
|
type: typeLabel,
|
|
113
164
|
visibility: "internal", // Default visibility for storage vars
|
|
114
|
-
}
|
|
115
|
-
})
|
|
165
|
+
}
|
|
166
|
+
})
|
|
116
167
|
|
|
117
168
|
// Detect access control pattern
|
|
118
|
-
result.accessControlPattern = detectAccessControlPattern(result.functions)
|
|
169
|
+
result.accessControlPattern = detectAccessControlPattern(result.functions)
|
|
119
170
|
|
|
120
|
-
return result
|
|
171
|
+
return result
|
|
121
172
|
} catch (e) {
|
|
122
|
-
result.error = `Unexpected error: ${e instanceof Error ? e.message : "Unknown error"}
|
|
123
|
-
return result
|
|
173
|
+
result.error = `Unexpected error: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
174
|
+
return result
|
|
124
175
|
}
|
|
125
176
|
}
|
|
126
177
|
|
|
@@ -128,18 +179,16 @@ export async function extractContractInfo(
|
|
|
128
179
|
* Map Solidity stateMutability to visibility
|
|
129
180
|
* ABI doesn't directly specify visibility, so we infer from mutability
|
|
130
181
|
*/
|
|
131
|
-
function mapStateMutabilityToVisibility(
|
|
132
|
-
stateMutability: string
|
|
133
|
-
): string {
|
|
182
|
+
function mapStateMutabilityToVisibility(stateMutability: string): string {
|
|
134
183
|
switch (stateMutability) {
|
|
135
184
|
case "pure":
|
|
136
185
|
case "view":
|
|
137
|
-
return "view"
|
|
186
|
+
return "view"
|
|
138
187
|
case "payable":
|
|
139
188
|
case "nonpayable":
|
|
140
|
-
return "external"
|
|
189
|
+
return "external"
|
|
141
190
|
default:
|
|
142
|
-
return "external"
|
|
191
|
+
return "external"
|
|
143
192
|
}
|
|
144
193
|
}
|
|
145
194
|
|
|
@@ -147,28 +196,24 @@ function mapStateMutabilityToVisibility(
|
|
|
147
196
|
* Detect access control pattern from function names and signatures
|
|
148
197
|
*/
|
|
149
198
|
function detectAccessControlPattern(
|
|
150
|
-
functions: Array<{ name: string; visibility: string; mutability: string; modifiers: string[] }
|
|
199
|
+
functions: Array<{ name: string; visibility: string; mutability: string; modifiers: string[] }>,
|
|
151
200
|
): "ownable" | "access-control" | "custom" | "none" {
|
|
152
|
-
const functionNames = functions.map((f) => f.name.toLowerCase())
|
|
201
|
+
const functionNames = functions.map((f) => f.name.toLowerCase())
|
|
153
202
|
|
|
154
203
|
// Check for Ownable pattern
|
|
155
204
|
if (functionNames.includes("owner") || functionNames.includes("transferownership")) {
|
|
156
|
-
return "ownable"
|
|
205
|
+
return "ownable"
|
|
157
206
|
}
|
|
158
207
|
|
|
159
208
|
// Check for AccessControl pattern (OpenZeppelin)
|
|
160
209
|
if (functionNames.includes("hasrole") || functionNames.includes("grantrole")) {
|
|
161
|
-
return "access-control"
|
|
210
|
+
return "access-control"
|
|
162
211
|
}
|
|
163
212
|
|
|
164
213
|
// Check for custom access control patterns
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
name.includes("onlyadmin") || name.includes("requireadmin")
|
|
168
|
-
)
|
|
169
|
-
) {
|
|
170
|
-
return "custom";
|
|
214
|
+
if (functionNames.some((name) => name.includes("onlyadmin") || name.includes("requireadmin"))) {
|
|
215
|
+
return "custom"
|
|
171
216
|
}
|
|
172
217
|
|
|
173
|
-
return "none"
|
|
218
|
+
return "none"
|
|
174
219
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface SoloditHealthStatus {
|
|
2
|
+
reachable: boolean
|
|
3
|
+
enabled: boolean
|
|
4
|
+
port: number
|
|
5
|
+
error?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function checkSoloditHealth(
|
|
9
|
+
port: number,
|
|
10
|
+
enabled: boolean,
|
|
11
|
+
): Promise<SoloditHealthStatus> {
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
return { reachable: false, enabled: false, port }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
18
|
+
signal: AbortSignal.timeout(2000),
|
|
19
|
+
})
|
|
20
|
+
return { reachable: response.ok, enabled: true, port }
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
reachable: false,
|
|
24
|
+
enabled: true,
|
|
25
|
+
port,
|
|
26
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|