solidity-argus 0.2.0 → 0.3.2
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 +93 -37
- package/package.json +34 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- 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/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -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 +2 -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 +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- 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 +1 -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 +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -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 +2 -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 +2 -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 +34 -7
- package/src/agents/pythia-prompt.ts +13 -4
- package/src/agents/scribe-prompt.ts +20 -2
- package/src/agents/sentinel-prompt.ts +45 -5
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- 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 +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- 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 +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- 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/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/system-prompt-hook.ts +18 -1
- package/src/hooks/tool-tracking-hook.ts +110 -51
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +24 -37
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- 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 +16 -3
- 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 +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +203 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- package/src/tools/contract-analyzer-tool.ts +142 -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 +201 -158
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +203 -191
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/pattern-schema.ts +3 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +305 -206
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +235 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +175 -75
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as parser from "@solidity-parser/parser"
|
|
2
|
+
import type { ContractProfile } from "../state/types"
|
|
3
|
+
|
|
4
|
+
const EXTERNAL_CALL_METHODS = new Set(["call", "transfer", "send", "delegatecall", "staticcall"])
|
|
2
5
|
|
|
3
6
|
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
|
|
7
|
+
type: string
|
|
8
|
+
name: string
|
|
9
|
+
inputs?: Array<{ name: string; type: string }>
|
|
10
|
+
outputs?: Array<{ name: string; type: string }>
|
|
11
|
+
stateMutability?: string
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
interface StorageLayoutItem {
|
|
12
|
-
label: string
|
|
13
|
-
type: string
|
|
14
|
-
slot: string
|
|
15
|
+
label: string
|
|
16
|
+
type: string
|
|
17
|
+
slot: string
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
interface StorageLayout {
|
|
18
|
-
storage: StorageLayoutItem[]
|
|
19
|
-
types: Record<string, { label: string }
|
|
21
|
+
storage: StorageLayoutItem[]
|
|
22
|
+
types: Record<string, { label: string }>
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -24,13 +27,117 @@ interface StorageLayout {
|
|
|
24
27
|
* prefix (e.g. forge table-format output, compilation progress).
|
|
25
28
|
* Falls back to the original string if no JSON delimiter is found.
|
|
26
29
|
*/
|
|
27
|
-
function extractJson(raw: string, opener: "[" | "{"): string {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
export function extractJson(raw: string, opener: "[" | "{"): string {
|
|
31
|
+
const start = raw.indexOf(opener)
|
|
32
|
+
if (start === -1) return raw
|
|
33
|
+
|
|
34
|
+
let depth = 0
|
|
35
|
+
let inString = false
|
|
36
|
+
let escaped = false
|
|
37
|
+
|
|
38
|
+
for (let i = start; i < raw.length; i++) {
|
|
39
|
+
const ch = raw.charAt(i)
|
|
40
|
+
|
|
41
|
+
if (inString) {
|
|
42
|
+
if (escaped) {
|
|
43
|
+
escaped = false
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (ch === "\\") {
|
|
47
|
+
escaped = true
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (ch === '"') {
|
|
51
|
+
inString = false
|
|
52
|
+
}
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (ch === '"') {
|
|
57
|
+
inString = true
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ch === "{" || ch === "[") {
|
|
62
|
+
depth++
|
|
63
|
+
} else if (ch === "}" || ch === "]") {
|
|
64
|
+
depth--
|
|
65
|
+
if (depth === 0) {
|
|
66
|
+
return raw.slice(start, i + 1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return raw
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toRecord(value: unknown): Record<string, unknown> | undefined {
|
|
75
|
+
if (typeof value === "object" && value !== null) {
|
|
76
|
+
return value as Record<string, unknown>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractNodeExpressionName(node: unknown): string | undefined {
|
|
83
|
+
const record = toRecord(node)
|
|
84
|
+
if (!record) return undefined
|
|
85
|
+
|
|
86
|
+
const type = typeof record.type === "string" ? record.type : undefined
|
|
87
|
+
if (!type) return undefined
|
|
88
|
+
|
|
89
|
+
if (type === "Identifier") {
|
|
90
|
+
return typeof record.name === "string" ? record.name : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (type === "ThisExpression") {
|
|
94
|
+
return "this"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (type === "MemberAccess") {
|
|
98
|
+
const expressionName = extractNodeExpressionName(record.expression)
|
|
99
|
+
const memberName = typeof record.memberName === "string" ? record.memberName : undefined
|
|
100
|
+
|
|
101
|
+
if (expressionName && memberName) {
|
|
102
|
+
return `${expressionName}.${memberName}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return expressionName ?? memberName
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (type === "IndexAccess") {
|
|
109
|
+
return extractNodeExpressionName(record.base)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (type === "FunctionCall") {
|
|
113
|
+
return extractNodeExpressionName(record.expression)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function parseExternalCalls(sourceText: string): string[] {
|
|
120
|
+
try {
|
|
121
|
+
const ast = parser.parse(sourceText, { tolerant: true, loc: false, range: false })
|
|
122
|
+
const externalCalls = new Set<string>()
|
|
123
|
+
|
|
124
|
+
parser.visit(ast, {
|
|
125
|
+
MemberAccess(node: unknown) {
|
|
126
|
+
const record = toRecord(node)
|
|
127
|
+
if (!record) return
|
|
128
|
+
|
|
129
|
+
const memberName = typeof record.memberName === "string" ? record.memberName : undefined
|
|
130
|
+
if (!memberName || !EXTERNAL_CALL_METHODS.has(memberName)) return
|
|
131
|
+
|
|
132
|
+
const expressionName = extractNodeExpressionName(record.expression)
|
|
133
|
+
externalCalls.add(expressionName ? `${expressionName}.${memberName}` : memberName)
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return [...externalCalls]
|
|
138
|
+
} catch {
|
|
139
|
+
return []
|
|
140
|
+
}
|
|
34
141
|
}
|
|
35
142
|
|
|
36
143
|
/**
|
|
@@ -41,7 +148,7 @@ function extractJson(raw: string, opener: "[" | "{"): string {
|
|
|
41
148
|
*/
|
|
42
149
|
export async function extractContractInfo(
|
|
43
150
|
contractName: string,
|
|
44
|
-
projectDir: string
|
|
151
|
+
projectDir: string,
|
|
45
152
|
): Promise<ContractProfile> {
|
|
46
153
|
const result: ContractProfile = {
|
|
47
154
|
name: contractName,
|
|
@@ -52,23 +159,21 @@ export async function extractContractInfo(
|
|
|
52
159
|
accessControlPattern: "none",
|
|
53
160
|
externalCalls: [],
|
|
54
161
|
riskIndicators: [],
|
|
55
|
-
}
|
|
162
|
+
}
|
|
56
163
|
|
|
57
164
|
try {
|
|
58
165
|
// Run forge inspect abi
|
|
59
|
-
const abiResult = Bun.spawnSync(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
);
|
|
166
|
+
const abiResult = Bun.spawnSync(["forge", "inspect", contractName, "abi", "--json"], {
|
|
167
|
+
cwd: projectDir,
|
|
168
|
+
stdout: "pipe",
|
|
169
|
+
stderr: "pipe",
|
|
170
|
+
timeout: 15_000,
|
|
171
|
+
})
|
|
67
172
|
|
|
68
173
|
if (!abiResult.success) {
|
|
69
|
-
const errorMsg = abiResult.stderr?.toString() || "Unknown error"
|
|
70
|
-
result.error = `Failed to inspect ABI: ${errorMsg}
|
|
71
|
-
return result
|
|
174
|
+
const errorMsg = abiResult.stderr?.toString() || "Unknown error"
|
|
175
|
+
result.error = `Failed to inspect ABI: ${errorMsg}`
|
|
176
|
+
return result
|
|
72
177
|
}
|
|
73
178
|
|
|
74
179
|
// Run forge inspect storage-layout
|
|
@@ -78,65 +183,66 @@ export async function extractContractInfo(
|
|
|
78
183
|
cwd: projectDir,
|
|
79
184
|
stdout: "pipe",
|
|
80
185
|
stderr: "pipe",
|
|
81
|
-
|
|
82
|
-
|
|
186
|
+
timeout: 15_000,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
83
189
|
|
|
84
190
|
if (!storageResult.success) {
|
|
85
|
-
const errorMsg = storageResult.stderr?.toString() || "Unknown error"
|
|
86
|
-
result.error = `Failed to inspect storage layout: ${errorMsg}
|
|
87
|
-
return result
|
|
191
|
+
const errorMsg = storageResult.stderr?.toString() || "Unknown error"
|
|
192
|
+
result.error = `Failed to inspect storage layout: ${errorMsg}`
|
|
193
|
+
return result
|
|
88
194
|
}
|
|
89
195
|
|
|
90
196
|
// Parse ABI
|
|
91
|
-
const abiRaw = abiResult.stdout?.toString() || "[]"
|
|
92
|
-
const abiOutput = extractJson(abiRaw, "[")
|
|
93
|
-
let abi: ABIFunction[] = []
|
|
197
|
+
const abiRaw = abiResult.stdout?.toString() || "[]"
|
|
198
|
+
const abiOutput = extractJson(abiRaw, "[")
|
|
199
|
+
let abi: ABIFunction[] = []
|
|
94
200
|
try {
|
|
95
|
-
abi = JSON.parse(abiOutput)
|
|
201
|
+
abi = JSON.parse(abiOutput)
|
|
96
202
|
} catch (e) {
|
|
97
|
-
result.error = `Failed to parse ABI JSON: ${e instanceof Error ? e.message : "Unknown error"}
|
|
98
|
-
return result
|
|
203
|
+
result.error = `Failed to parse ABI JSON: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
204
|
+
return result
|
|
99
205
|
}
|
|
100
206
|
|
|
101
207
|
// Parse storage layout
|
|
102
|
-
const storageRaw = storageResult.stdout?.toString() || "{}"
|
|
103
|
-
const storageOutput = extractJson(storageRaw, "{")
|
|
104
|
-
let storageLayout: StorageLayout = { storage: [], types: {} }
|
|
208
|
+
const storageRaw = storageResult.stdout?.toString() || "{}"
|
|
209
|
+
const storageOutput = extractJson(storageRaw, "{")
|
|
210
|
+
let storageLayout: StorageLayout = { storage: [], types: {} }
|
|
105
211
|
try {
|
|
106
|
-
storageLayout = JSON.parse(storageOutput)
|
|
212
|
+
storageLayout = JSON.parse(storageOutput)
|
|
107
213
|
} catch (e) {
|
|
108
|
-
result.error = `Failed to parse storage layout JSON: ${e instanceof Error ? e.message : "Unknown error"}
|
|
109
|
-
return result
|
|
214
|
+
result.error = `Failed to parse storage layout JSON: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
215
|
+
return result
|
|
110
216
|
}
|
|
111
217
|
|
|
112
218
|
// Extract functions from ABI
|
|
113
|
-
const functions = abi.filter((item) => item.type === "function")
|
|
219
|
+
const functions = abi.filter((item) => item.type === "function")
|
|
114
220
|
result.functions = functions.map((func) => ({
|
|
115
221
|
name: func.name || "",
|
|
116
222
|
visibility: mapStateMutabilityToVisibility(func.stateMutability || "nonpayable"),
|
|
117
223
|
mutability: func.stateMutability || "nonpayable",
|
|
118
224
|
modifiers: [],
|
|
119
|
-
}))
|
|
225
|
+
}))
|
|
120
226
|
|
|
121
227
|
// Extract state variables from storage layout
|
|
122
228
|
result.stateVars = storageLayout.storage.map((item) => {
|
|
123
|
-
const typeInfo = storageLayout.types[item.type]
|
|
124
|
-
const typeLabel = typeInfo?.label || item.type
|
|
229
|
+
const typeInfo = storageLayout.types[item.type]
|
|
230
|
+
const typeLabel = typeInfo?.label || item.type
|
|
125
231
|
|
|
126
232
|
return {
|
|
127
233
|
name: item.label,
|
|
128
234
|
type: typeLabel,
|
|
129
235
|
visibility: "internal", // Default visibility for storage vars
|
|
130
|
-
}
|
|
131
|
-
})
|
|
236
|
+
}
|
|
237
|
+
})
|
|
132
238
|
|
|
133
239
|
// Detect access control pattern
|
|
134
|
-
result.accessControlPattern = detectAccessControlPattern(result.functions)
|
|
240
|
+
result.accessControlPattern = detectAccessControlPattern(result.functions)
|
|
135
241
|
|
|
136
|
-
return result
|
|
242
|
+
return result
|
|
137
243
|
} catch (e) {
|
|
138
|
-
result.error = `Unexpected error: ${e instanceof Error ? e.message : "Unknown error"}
|
|
139
|
-
return result
|
|
244
|
+
result.error = `Unexpected error: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
245
|
+
return result
|
|
140
246
|
}
|
|
141
247
|
}
|
|
142
248
|
|
|
@@ -144,18 +250,16 @@ export async function extractContractInfo(
|
|
|
144
250
|
* Map Solidity stateMutability to visibility
|
|
145
251
|
* ABI doesn't directly specify visibility, so we infer from mutability
|
|
146
252
|
*/
|
|
147
|
-
function mapStateMutabilityToVisibility(
|
|
148
|
-
stateMutability: string
|
|
149
|
-
): string {
|
|
253
|
+
function mapStateMutabilityToVisibility(stateMutability: string): string {
|
|
150
254
|
switch (stateMutability) {
|
|
151
255
|
case "pure":
|
|
152
256
|
case "view":
|
|
153
|
-
return "view"
|
|
257
|
+
return "view"
|
|
154
258
|
case "payable":
|
|
155
259
|
case "nonpayable":
|
|
156
|
-
return "external"
|
|
260
|
+
return "external"
|
|
157
261
|
default:
|
|
158
|
-
return "external"
|
|
262
|
+
return "external"
|
|
159
263
|
}
|
|
160
264
|
}
|
|
161
265
|
|
|
@@ -163,28 +267,24 @@ function mapStateMutabilityToVisibility(
|
|
|
163
267
|
* Detect access control pattern from function names and signatures
|
|
164
268
|
*/
|
|
165
269
|
function detectAccessControlPattern(
|
|
166
|
-
functions: Array<{ name: string; visibility: string; mutability: string; modifiers: string[] }
|
|
270
|
+
functions: Array<{ name: string; visibility: string; mutability: string; modifiers: string[] }>,
|
|
167
271
|
): "ownable" | "access-control" | "custom" | "none" {
|
|
168
|
-
const functionNames = functions.map((f) => f.name.toLowerCase())
|
|
272
|
+
const functionNames = functions.map((f) => f.name.toLowerCase())
|
|
169
273
|
|
|
170
274
|
// Check for Ownable pattern
|
|
171
275
|
if (functionNames.includes("owner") || functionNames.includes("transferownership")) {
|
|
172
|
-
return "ownable"
|
|
276
|
+
return "ownable"
|
|
173
277
|
}
|
|
174
278
|
|
|
175
279
|
// Check for AccessControl pattern (OpenZeppelin)
|
|
176
280
|
if (functionNames.includes("hasrole") || functionNames.includes("grantrole")) {
|
|
177
|
-
return "access-control"
|
|
281
|
+
return "access-control"
|
|
178
282
|
}
|
|
179
283
|
|
|
180
284
|
// Check for custom access control patterns
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
name.includes("onlyadmin") || name.includes("requireadmin")
|
|
184
|
-
)
|
|
185
|
-
) {
|
|
186
|
-
return "custom";
|
|
285
|
+
if (functionNames.some((name) => name.includes("onlyadmin") || name.includes("requireadmin"))) {
|
|
286
|
+
return "custom"
|
|
187
287
|
}
|
|
188
288
|
|
|
189
|
-
return "none"
|
|
289
|
+
return "none"
|
|
190
290
|
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
pack_name: access-control
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: missing-access-modifier
|
|
5
|
-
category: access-control
|
|
6
|
-
severity: High
|
|
7
|
-
swc: SWC-105
|
|
8
|
-
confidence: Low
|
|
9
|
-
version: "1.0"
|
|
10
|
-
regex: 'function\s+\w+\s*\([^)]*\)\s+(external|public)'
|
|
11
|
-
description: External or public function — verify appropriate access control modifiers (onlyOwner, onlyRole, require(msg.sender)) are applied
|
|
12
|
-
remediation: Add access control modifiers to sensitive functions; use OpenZeppelin AccessControl or Ownable patterns
|
|
13
|
-
|
|
14
|
-
- name: unprotected-initialize
|
|
15
|
-
category: access-control
|
|
16
|
-
severity: Critical
|
|
17
|
-
confidence: High
|
|
18
|
-
version: "1.0"
|
|
19
|
-
regex: 'function\s+initialize'
|
|
20
|
-
description: Initializer function detected — if missing initializer modifier, anyone can call and take ownership of the contract
|
|
21
|
-
remediation: Use OpenZeppelin Initializable with initializer modifier; call _disableInitializers() in constructor for implementation contracts
|
|
22
|
-
|
|
23
|
-
- name: default-visibility
|
|
24
|
-
category: access-control
|
|
25
|
-
severity: Medium
|
|
26
|
-
swc: SWC-100
|
|
27
|
-
confidence: Low
|
|
28
|
-
version: "1.0"
|
|
29
|
-
regex: 'function\s+\w+\s*\([^)]*\)\s*\{'
|
|
30
|
-
description: Function without explicit visibility specifier — defaults to public in older Solidity versions, potentially exposing internal logic
|
|
31
|
-
remediation: Always specify visibility (external, public, internal, private) explicitly for every function
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
pack_name: erc4626
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: inflation-attack
|
|
5
|
-
category: erc4626
|
|
6
|
-
severity: Critical
|
|
7
|
-
confidence: High
|
|
8
|
-
version: "1.0"
|
|
9
|
-
regex: 'deposit.*totalSupply.*==.*0|convertToShares.*totalSupply'
|
|
10
|
-
description: ERC-4626 vault first-depositor inflation attack — attacker can donate assets to vault before first deposit to manipulate share price and steal subsequent deposits
|
|
11
|
-
remediation: Mint dead shares on first deposit (e.g., 10**3 to address(0)); use virtual offset in share calculation; set minimum deposit amount
|
|
12
|
-
|
|
13
|
-
- name: donation-attack
|
|
14
|
-
category: erc4626
|
|
15
|
-
severity: High
|
|
16
|
-
confidence: Medium
|
|
17
|
-
version: "1.0"
|
|
18
|
-
regex: 'balanceOf.*address.*this.*totalAssets|asset\.balanceOf'
|
|
19
|
-
description: Vault totalAssets derived from balanceOf — vulnerable to donation attack where attacker sends assets directly to inflate share price
|
|
20
|
-
remediation: Use internal accounting for totalAssets instead of balanceOf; track deposits and withdrawals explicitly
|
|
21
|
-
|
|
22
|
-
- name: rounding-error
|
|
23
|
-
category: erc4626
|
|
24
|
-
severity: Medium
|
|
25
|
-
confidence: Medium
|
|
26
|
-
version: "1.0"
|
|
27
|
-
regex: 'mulDiv|roundUp|roundDown|FullMath'
|
|
28
|
-
description: Custom rounding math in vault share calculations — potential rounding errors that favor attacker (round down on deposit, round up on withdraw)
|
|
29
|
-
remediation: Round against the user (down on deposit/mint, up on withdraw/redeem); use OpenZeppelin Math.mulDiv with explicit rounding direction
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
pack_name: flash-loan
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: unchecked-flash-return
|
|
5
|
-
category: flash-loan
|
|
6
|
-
severity: High
|
|
7
|
-
confidence: Medium
|
|
8
|
-
version: "1.0"
|
|
9
|
-
regex: 'flashLoan|flashBorrow'
|
|
10
|
-
description: Flash loan invocation without verified return — borrowed funds may not be repaid if return value is not checked
|
|
11
|
-
remediation: Verify flash loan callback returns expected success value; check token balance after repayment; use established flash loan receiver interfaces
|
|
12
|
-
|
|
13
|
-
- name: balance-inflation
|
|
14
|
-
category: flash-loan
|
|
15
|
-
severity: Medium
|
|
16
|
-
confidence: Medium
|
|
17
|
-
version: "1.0"
|
|
18
|
-
regex: 'balanceOf\(address\(this\)\)'
|
|
19
|
-
description: Contract reads its own token balance — vulnerable to donation/inflation attacks where attacker sends tokens directly to manipulate balance-dependent logic
|
|
20
|
-
remediation: Track balances via internal accounting instead of balanceOf(address(this)); use shares-based accounting for vaults
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
pack_name: oracle-manipulation
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: stale-price-check
|
|
5
|
-
category: oracle-manipulation
|
|
6
|
-
severity: High
|
|
7
|
-
swc: SWC-120
|
|
8
|
-
confidence: High
|
|
9
|
-
version: "1.0"
|
|
10
|
-
regex: 'latestRoundData|getPrice'
|
|
11
|
-
description: Oracle price feed usage — verify staleness checks (updatedAt, roundId) are enforced to prevent using outdated prices
|
|
12
|
-
remediation: Add require(updatedAt > block.timestamp - MAX_STALENESS) after latestRoundData calls; check answeredInRound >= roundId
|
|
13
|
-
|
|
14
|
-
- name: twap-manipulation
|
|
15
|
-
category: oracle-manipulation
|
|
16
|
-
severity: Medium
|
|
17
|
-
confidence: Medium
|
|
18
|
-
version: "1.0"
|
|
19
|
-
regex: 'observe\(|consult\('
|
|
20
|
-
description: TWAP oracle usage — time-weighted average prices can be manipulated via sustained trading pressure within the observation window
|
|
21
|
-
remediation: Use sufficiently long TWAP windows (30+ minutes); combine with spot price deviation checks; add circuit breakers
|
|
22
|
-
|
|
23
|
-
- name: price-feed-decimals
|
|
24
|
-
category: oracle-manipulation
|
|
25
|
-
severity: Medium
|
|
26
|
-
confidence: Medium
|
|
27
|
-
version: "1.0"
|
|
28
|
-
regex: 'priceFeed|oracle.*decimals'
|
|
29
|
-
description: Oracle price feed with decimal handling — potential decimal mismatch between oracle feed (8 decimals) and token (18 decimals)
|
|
30
|
-
remediation: Normalize oracle response to consistent decimals; use oracle.decimals() dynamically rather than hardcoded values
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
pack_name: proxy
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: storage-collision
|
|
5
|
-
category: proxy
|
|
6
|
-
severity: Critical
|
|
7
|
-
swc: SWC-112
|
|
8
|
-
confidence: Medium
|
|
9
|
-
version: "1.0"
|
|
10
|
-
regex: 'delegatecall|IMPLEMENTATION_SLOT'
|
|
11
|
-
description: Delegatecall or implementation slot usage — potential storage collision between proxy and implementation contracts if storage layouts diverge
|
|
12
|
-
remediation: Use EIP-1967 standard storage slots; use OpenZeppelin TransparentUpgradeableProxy or UUPS; verify storage layout compatibility on upgrades
|
|
13
|
-
|
|
14
|
-
- name: uninitialized-proxy
|
|
15
|
-
category: proxy
|
|
16
|
-
severity: High
|
|
17
|
-
confidence: Medium
|
|
18
|
-
version: "1.0"
|
|
19
|
-
regex: '_disableInitializers|initializer'
|
|
20
|
-
description: Proxy initialization pattern detected — verify implementation contract calls _disableInitializers() in constructor and proxy calls initialize()
|
|
21
|
-
remediation: Call _disableInitializers() in implementation constructor; ensure initialize() is called atomically during proxy deployment
|
|
22
|
-
|
|
23
|
-
- name: selector-clash
|
|
24
|
-
category: proxy
|
|
25
|
-
severity: Medium
|
|
26
|
-
confidence: Low
|
|
27
|
-
version: "1.0"
|
|
28
|
-
regex: 'fallback\(\)|receive\(\).*delegatecall'
|
|
29
|
-
description: Fallback or receive function with delegatecall — risk of function selector clash between proxy admin functions and implementation functions
|
|
30
|
-
remediation: Use TransparentUpgradeableProxy pattern to separate admin and user call paths; verify no selector collisions with implementation ABI
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
pack_name: reentrancy
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: reentrancy-eth-transfer
|
|
5
|
-
category: reentrancy
|
|
6
|
-
severity: High
|
|
7
|
-
swc: SWC-107
|
|
8
|
-
confidence: High
|
|
9
|
-
version: "1.0"
|
|
10
|
-
regex: '\.call\{value:'
|
|
11
|
-
description: ETH transfer via low-level call — classic reentrancy vector where external call is made before state updates
|
|
12
|
-
remediation: Apply checks-effects-interactions pattern; use ReentrancyGuard; update state before external calls
|
|
13
|
-
|
|
14
|
-
- name: reentrancy-erc20
|
|
15
|
-
category: reentrancy
|
|
16
|
-
severity: Medium
|
|
17
|
-
confidence: Medium
|
|
18
|
-
version: "1.0"
|
|
19
|
-
regex: '\.(transfer|transferFrom)\('
|
|
20
|
-
description: ERC-20 token transfer that may precede state changes — potential reentrancy via token callback hooks (ERC-777, ERC-1155)
|
|
21
|
-
remediation: Update state variables before token transfers; use ReentrancyGuard for functions with external token interactions
|
|
22
|
-
|
|
23
|
-
- name: cross-function-reentrancy
|
|
24
|
-
category: reentrancy
|
|
25
|
-
severity: High
|
|
26
|
-
confidence: Low
|
|
27
|
-
version: "1.0"
|
|
28
|
-
regex: '(external|public)\s.*\{[^}]*\.call'
|
|
29
|
-
description: Public or external function containing a low-level call — potential cross-function reentrancy if shared state is read by other functions
|
|
30
|
-
remediation: Use ReentrancyGuard on all functions sharing mutable state; apply checks-effects-interactions across the contract
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
pack_name: signature
|
|
2
|
-
pack_version: "1.0"
|
|
3
|
-
patterns:
|
|
4
|
-
- name: replay-attack
|
|
5
|
-
category: signature
|
|
6
|
-
severity: High
|
|
7
|
-
swc: SWC-117
|
|
8
|
-
confidence: Medium
|
|
9
|
-
version: "1.0"
|
|
10
|
-
regex: 'ecrecover|ECDSA\.recover'
|
|
11
|
-
description: Signature recovery without nonce tracking — signatures may be replayed across transactions or chains if nonce and chainId are not included in signed data
|
|
12
|
-
remediation: Include nonce, chainId, and contract address in signed message hash; increment nonce after use; use EIP-712 typed structured data
|
|
13
|
-
|
|
14
|
-
- name: sig-malleability
|
|
15
|
-
category: signature
|
|
16
|
-
severity: Medium
|
|
17
|
-
swc: SWC-117
|
|
18
|
-
confidence: Medium
|
|
19
|
-
version: "1.0"
|
|
20
|
-
regex: ecrecover
|
|
21
|
-
description: Raw ecrecover usage — ECDSA signatures are malleable (s-value can be flipped) allowing signature reuse if not checked against canonical form
|
|
22
|
-
remediation: Use OpenZeppelin ECDSA.recover which enforces s <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; reject non-canonical signatures
|
|
23
|
-
|
|
24
|
-
- name: missing-nonce
|
|
25
|
-
category: signature
|
|
26
|
-
severity: High
|
|
27
|
-
confidence: Medium
|
|
28
|
-
version: "1.0"
|
|
29
|
-
regex: 'permit\(|signTypedData'
|
|
30
|
-
description: Permit or typed data signing without nonce validation — missing nonce allows signature replay after the original transaction is executed
|
|
31
|
-
remediation: Track per-address nonces mapping(address => uint256); include nonce in EIP-712 struct; increment nonce on each use
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { AuditState } from "../state/types"
|
|
2
|
-
import { createAuditState } from "../state/audit-state"
|
|
3
|
-
import { createLogger } from "../shared/logger"
|
|
4
|
-
|
|
5
|
-
export type AuditEventType =
|
|
6
|
-
| "session.created"
|
|
7
|
-
| "session.idle"
|
|
8
|
-
| "session.error"
|
|
9
|
-
| "session.deleted"
|
|
10
|
-
| "audit.phase-changed"
|
|
11
|
-
| "audit.finding-added"
|
|
12
|
-
| "audit.complete"
|
|
13
|
-
|
|
14
|
-
export type EventHookV2Fn = (input: {
|
|
15
|
-
event: { type: string; sessionId?: string; properties?: Record<string, unknown> }
|
|
16
|
-
}) => Promise<void>
|
|
17
|
-
|
|
18
|
-
export type EventSubHandler = (event: {
|
|
19
|
-
type: string
|
|
20
|
-
sessionId?: string
|
|
21
|
-
auditState: AuditState | null
|
|
22
|
-
setAuditState: (state: AuditState | null) => void
|
|
23
|
-
}) => Promise<void>
|
|
24
|
-
|
|
25
|
-
export function createEventHookV2(
|
|
26
|
-
projectDir?: string,
|
|
27
|
-
subHandlers: EventSubHandler[] = [],
|
|
28
|
-
): {
|
|
29
|
-
hook: EventHookV2Fn
|
|
30
|
-
getAuditState: () => AuditState | null
|
|
31
|
-
setAuditState: (state: AuditState | null) => void
|
|
32
|
-
} {
|
|
33
|
-
const logger = createLogger()
|
|
34
|
-
let currentAuditState: AuditState | null = null
|
|
35
|
-
|
|
36
|
-
const getAuditState = (): AuditState | null => currentAuditState
|
|
37
|
-
const setAuditState = (state: AuditState | null): void => {
|
|
38
|
-
currentAuditState = state
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const hook: EventHookV2Fn = async (input): Promise<void> => {
|
|
42
|
-
const { type, sessionId } = input.event
|
|
43
|
-
|
|
44
|
-
switch (type) {
|
|
45
|
-
case "session.created": {
|
|
46
|
-
const dir = projectDir ?? process.cwd()
|
|
47
|
-
const { state } = createAuditState(dir)
|
|
48
|
-
currentAuditState = state
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
case "session.idle": {
|
|
53
|
-
if (currentAuditState) {
|
|
54
|
-
logger.debug(
|
|
55
|
-
`Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}`,
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
break
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
case "session.error": {
|
|
62
|
-
if (currentAuditState) {
|
|
63
|
-
logger.error(
|
|
64
|
-
`Session error — state snapshot: ${JSON.stringify({
|
|
65
|
-
sessionId: currentAuditState.sessionId,
|
|
66
|
-
phase: currentAuditState.currentPhase,
|
|
67
|
-
findingsCount: currentAuditState.findings.length,
|
|
68
|
-
contractsReviewed: currentAuditState.contractsReviewed,
|
|
69
|
-
})}`,
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
break
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
case "session.deleted": {
|
|
76
|
-
currentAuditState = null
|
|
77
|
-
break
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
default:
|
|
81
|
-
break
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (const handler of subHandlers) {
|
|
85
|
-
try {
|
|
86
|
-
await handler({
|
|
87
|
-
type,
|
|
88
|
-
sessionId,
|
|
89
|
-
auditState: currentAuditState,
|
|
90
|
-
setAuditState,
|
|
91
|
-
})
|
|
92
|
-
} catch (error) {
|
|
93
|
-
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { hook, getAuditState, setAuditState }
|
|
99
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { ArgusConfig } from "../config/types";
|
|
2
|
-
import type { Managers } from "../managers/types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* PluginState interface
|
|
6
|
-
* Represents the complete state of the Argus plugin instance
|
|
7
|
-
* Includes configuration, project context, and manager instances
|
|
8
|
-
*/
|
|
9
|
-
export interface PluginState {
|
|
10
|
-
config: ArgusConfig;
|
|
11
|
-
projectDir: string;
|
|
12
|
-
managers: Managers;
|
|
13
|
-
isHookEnabled: (name: string) => boolean;
|
|
14
|
-
}
|