web3crit-scanner 7.0.1
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/README.md +685 -0
- package/bin/web3crit +10 -0
- package/package.json +59 -0
- package/src/analyzers/control-flow.js +256 -0
- package/src/analyzers/data-flow.js +720 -0
- package/src/analyzers/exploit-chain.js +751 -0
- package/src/analyzers/immunefi-classifier.js +515 -0
- package/src/analyzers/poc-validator.js +396 -0
- package/src/analyzers/solodit-enricher.js +1122 -0
- package/src/cli.js +546 -0
- package/src/detectors/access-control-enhanced.js +458 -0
- package/src/detectors/base-detector.js +213 -0
- package/src/detectors/callback-reentrancy.js +362 -0
- package/src/detectors/cross-contract-reentrancy.js +697 -0
- package/src/detectors/delegatecall.js +167 -0
- package/src/detectors/deprecated-functions.js +62 -0
- package/src/detectors/flash-loan.js +408 -0
- package/src/detectors/frontrunning.js +553 -0
- package/src/detectors/gas-griefing.js +701 -0
- package/src/detectors/governance-attacks.js +366 -0
- package/src/detectors/integer-overflow.js +487 -0
- package/src/detectors/oracle-manipulation.js +524 -0
- package/src/detectors/permit-exploits.js +368 -0
- package/src/detectors/precision-loss.js +408 -0
- package/src/detectors/price-manipulation-advanced.js +548 -0
- package/src/detectors/proxy-vulnerabilities.js +651 -0
- package/src/detectors/readonly-reentrancy.js +473 -0
- package/src/detectors/rebasing-token-vault.js +416 -0
- package/src/detectors/reentrancy-enhanced.js +359 -0
- package/src/detectors/selfdestruct.js +259 -0
- package/src/detectors/share-manipulation.js +412 -0
- package/src/detectors/signature-replay.js +409 -0
- package/src/detectors/storage-collision.js +446 -0
- package/src/detectors/timestamp-dependence.js +494 -0
- package/src/detectors/toctou.js +427 -0
- package/src/detectors/token-standard-compliance.js +465 -0
- package/src/detectors/unchecked-call.js +214 -0
- package/src/detectors/vault-inflation.js +421 -0
- package/src/index.js +42 -0
- package/src/package-lock.json +2874 -0
- package/src/package.json +39 -0
- package/src/scanner-enhanced.js +816 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token Standard Compliance Detector
|
|
5
|
+
* Ensures tokens strictly follow ERC standards (ERC20, ERC721, ERC1155)
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Missing required functions for ERC standards
|
|
9
|
+
* - Incorrect function signatures
|
|
10
|
+
* - Missing events
|
|
11
|
+
* - Non-standard return values
|
|
12
|
+
* - Missing approvals/transfers
|
|
13
|
+
* - Incorrect behavior patterns
|
|
14
|
+
*/
|
|
15
|
+
class TokenStandardComplianceDetector extends BaseDetector {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(
|
|
18
|
+
'Token Standard Compliance',
|
|
19
|
+
'Detects violations of ERC token standards (ERC20, ERC721, ERC1155)',
|
|
20
|
+
'HIGH'
|
|
21
|
+
);
|
|
22
|
+
this.currentContract = null;
|
|
23
|
+
this.tokenStandard = null; // 'ERC20', 'ERC721', 'ERC1155', or null
|
|
24
|
+
this.requiredFunctions = {
|
|
25
|
+
ERC20: ['totalSupply', 'balanceOf', 'transfer', 'transferFrom', 'approve', 'allowance'],
|
|
26
|
+
ERC721: ['balanceOf', 'ownerOf', 'safeTransferFrom', 'transferFrom', 'approve', 'setApprovalForAll', 'getApproved', 'isApprovedForAll'],
|
|
27
|
+
ERC1155: ['balanceOf', 'balanceOfBatch', 'setApprovalForAll', 'isApprovedForAll', 'safeTransferFrom', 'safeBatchTransferFrom']
|
|
28
|
+
};
|
|
29
|
+
this.requiredEvents = {
|
|
30
|
+
ERC20: ['Transfer', 'Approval'],
|
|
31
|
+
ERC721: ['Transfer', 'Approval', 'ApprovalForAll'],
|
|
32
|
+
ERC1155: ['TransferSingle', 'TransferBatch', 'ApprovalForAll', 'URI']
|
|
33
|
+
};
|
|
34
|
+
this.foundFunctions = new Set();
|
|
35
|
+
this.foundEvents = new Set();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
39
|
+
this.findings = [];
|
|
40
|
+
this.ast = ast;
|
|
41
|
+
this.sourceCode = sourceCode;
|
|
42
|
+
this.fileName = fileName;
|
|
43
|
+
this.sourceLines = sourceCode.split('\n');
|
|
44
|
+
this.cfg = cfg;
|
|
45
|
+
this.dataFlow = dataFlow;
|
|
46
|
+
|
|
47
|
+
// Reset per-file state
|
|
48
|
+
this.tokenStandard = null;
|
|
49
|
+
this.foundFunctions.clear();
|
|
50
|
+
this.foundEvents.clear();
|
|
51
|
+
this.currentContract = null;
|
|
52
|
+
|
|
53
|
+
// Detect which standard this contract implements
|
|
54
|
+
this.detectTokenStandard(sourceCode);
|
|
55
|
+
|
|
56
|
+
if (!this.tokenStandard) {
|
|
57
|
+
// Not a token contract, skip
|
|
58
|
+
return this.findings;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.traverse(ast);
|
|
62
|
+
|
|
63
|
+
// Post-traversal analysis
|
|
64
|
+
this.analyzeCompliance();
|
|
65
|
+
|
|
66
|
+
return this.findings;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
visitContractDefinition(node) {
|
|
70
|
+
this.currentContract = node.name;
|
|
71
|
+
this.foundFunctions.clear();
|
|
72
|
+
this.foundEvents.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visitFunctionDefinition(node) {
|
|
76
|
+
const funcName = node.name || '';
|
|
77
|
+
|
|
78
|
+
if (this.tokenStandard) {
|
|
79
|
+
// Check if this is a required function
|
|
80
|
+
const required = this.requiredFunctions[this.tokenStandard] || [];
|
|
81
|
+
if (required.includes(funcName)) {
|
|
82
|
+
this.foundFunctions.add(funcName);
|
|
83
|
+
|
|
84
|
+
// Validate function signature and behavior
|
|
85
|
+
this.validateFunction(node, funcName);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
visitEventDefinition(node) {
|
|
91
|
+
const eventName = node.name || '';
|
|
92
|
+
|
|
93
|
+
if (this.tokenStandard) {
|
|
94
|
+
const required = this.requiredEvents[this.tokenStandard] || [];
|
|
95
|
+
if (required.includes(eventName)) {
|
|
96
|
+
this.foundEvents.add(eventName);
|
|
97
|
+
|
|
98
|
+
// Validate event signature
|
|
99
|
+
this.validateEvent(node, eventName);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detect which ERC standard this contract implements
|
|
106
|
+
*/
|
|
107
|
+
detectTokenStandard(sourceCode) {
|
|
108
|
+
// Strip imports/comments so "ERC20" in import paths doesn't misclassify non-token contracts
|
|
109
|
+
const stripped = sourceCode
|
|
110
|
+
.replace(/^\s*import[^;]*;/gm, '')
|
|
111
|
+
.replace(/\/\/.*$/gm, '')
|
|
112
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
113
|
+
|
|
114
|
+
const codeLower = stripped.toLowerCase();
|
|
115
|
+
|
|
116
|
+
// Check for explicit ERC standard inheritance or interface implementation
|
|
117
|
+
const hasERCInterface =
|
|
118
|
+
/contract\s+\w+\s+(?:is|implements)\s+.*\b(IERC|ERC)\d+\b/i.test(stripped) ||
|
|
119
|
+
/contract\s+\w+\s+(?:is|implements)\s+.*\bERC\b/i.test(stripped);
|
|
120
|
+
|
|
121
|
+
// Check for ERC1155 (most specific)
|
|
122
|
+
if (codeLower.includes('erc1155') ||
|
|
123
|
+
codeLower.includes('multitoken') ||
|
|
124
|
+
(codeLower.includes('balanceofbatch') && codeLower.includes('safebatchtransferfrom'))) {
|
|
125
|
+
this.tokenStandard = 'ERC1155';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for ERC721
|
|
130
|
+
if (codeLower.includes('erc721') ||
|
|
131
|
+
(codeLower.includes('nft') && codeLower.includes('ownerof')) ||
|
|
132
|
+
(codeLower.includes('ownerof') && codeLower.includes('tokenuri')) ||
|
|
133
|
+
(codeLower.includes('setapprovalforall') && codeLower.includes('ownerof'))) {
|
|
134
|
+
this.tokenStandard = 'ERC721';
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for ERC20 - need multiple indicators to avoid false positives
|
|
139
|
+
// Must have both transfer/transferFrom AND approve/allowance patterns
|
|
140
|
+
const hasTransfer = /function\s+transfer/i.test(sourceCode) || /function\s+transferFrom/i.test(sourceCode);
|
|
141
|
+
const hasApprove = /function\s+approve/i.test(sourceCode) || /function\s+allowance/i.test(sourceCode);
|
|
142
|
+
const hasBalanceOf = /function\s+balanceOf/i.test(sourceCode);
|
|
143
|
+
const hasTotalSupply = /function\s+totalSupply/i.test(sourceCode);
|
|
144
|
+
|
|
145
|
+
// Only treat as ERC20 if it is explicitly implemented/inherited, or declares core ERC20 functions
|
|
146
|
+
if (hasERCInterface) {
|
|
147
|
+
this.tokenStandard = 'ERC20';
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only detect ERC20 if it has multiple token functions (not just one)
|
|
152
|
+
if ((hasTransfer && hasApprove && hasBalanceOf) ||
|
|
153
|
+
(hasTransfer && hasTotalSupply && hasBalanceOf)) {
|
|
154
|
+
this.tokenStandard = 'ERC20';
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate function implementation
|
|
161
|
+
*/
|
|
162
|
+
validateFunction(node, funcName) {
|
|
163
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
164
|
+
const funcCodeLower = funcCode.toLowerCase();
|
|
165
|
+
|
|
166
|
+
// ERC20 specific validations
|
|
167
|
+
if (this.tokenStandard === 'ERC20') {
|
|
168
|
+
this.validateERC20Function(node, funcName, funcCode);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ERC721 specific validations
|
|
172
|
+
if (this.tokenStandard === 'ERC721') {
|
|
173
|
+
this.validateERC721Function(node, funcName, funcCode);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ERC1155 specific validations
|
|
177
|
+
if (this.tokenStandard === 'ERC1155') {
|
|
178
|
+
this.validateERC1155Function(node, funcName, funcCode);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate ERC20 function
|
|
184
|
+
*/
|
|
185
|
+
validateERC20Function(node, funcName, code) {
|
|
186
|
+
const codeLower = code.toLowerCase();
|
|
187
|
+
|
|
188
|
+
// transfer/transferFrom must emit Transfer event
|
|
189
|
+
if ((funcName === 'transfer' || funcName === 'transferFrom') &&
|
|
190
|
+
!codeLower.includes('emit transfer')) {
|
|
191
|
+
this.addFinding({
|
|
192
|
+
title: 'ERC20 Transfer Missing Transfer Event',
|
|
193
|
+
description: `ERC20 function '${funcName}' does not emit Transfer event. This violates ERC20 standard and breaks compatibility with wallets, DEXs, and other contracts expecting the event.`,
|
|
194
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
195
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
196
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
197
|
+
code: this.getCodeSnippet(node.loc),
|
|
198
|
+
severity: 'HIGH',
|
|
199
|
+
confidence: 'HIGH',
|
|
200
|
+
exploitable: false,
|
|
201
|
+
exploitabilityScore: 30,
|
|
202
|
+
attackVector: 'standard-compliance',
|
|
203
|
+
recommendation: 'Emit Transfer event: emit Transfer(from, to, amount);',
|
|
204
|
+
references: [
|
|
205
|
+
'https://eips.ethereum.org/EIPS/eip-20',
|
|
206
|
+
'https://swcregistry.io/docs/SWC-140'
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// approve must emit Approval event
|
|
212
|
+
if (funcName === 'approve' && !codeLower.includes('emit approval')) {
|
|
213
|
+
this.addFinding({
|
|
214
|
+
title: 'ERC20 Approve Missing Approval Event',
|
|
215
|
+
description: `ERC20 function 'approve' does not emit Approval event. This violates ERC20 standard.`,
|
|
216
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
217
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
218
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
219
|
+
code: this.getCodeSnippet(node.loc),
|
|
220
|
+
severity: 'HIGH',
|
|
221
|
+
confidence: 'HIGH',
|
|
222
|
+
exploitable: false,
|
|
223
|
+
exploitabilityScore: 30,
|
|
224
|
+
attackVector: 'standard-compliance',
|
|
225
|
+
recommendation: 'Emit Approval event: emit Approval(owner, spender, amount);',
|
|
226
|
+
references: [
|
|
227
|
+
'https://eips.ethereum.org/EIPS/eip-20'
|
|
228
|
+
]
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check for non-standard return values (should return bool)
|
|
233
|
+
if ((funcName === 'transfer' || funcName === 'transferFrom' || funcName === 'approve') &&
|
|
234
|
+
node.returnParameters && node.returnParameters.length === 0) {
|
|
235
|
+
// Some ERC20 implementations don't return bool, but it's non-standard
|
|
236
|
+
this.addFinding({
|
|
237
|
+
title: 'ERC20 Function Missing Return Value',
|
|
238
|
+
description: `ERC20 function '${funcName}' should return bool according to standard. Missing return value may break compatibility with some contracts.`,
|
|
239
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
240
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
241
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
242
|
+
code: this.getCodeSnippet(node.loc),
|
|
243
|
+
severity: 'MEDIUM',
|
|
244
|
+
confidence: 'MEDIUM',
|
|
245
|
+
exploitable: false,
|
|
246
|
+
exploitabilityScore: 20,
|
|
247
|
+
attackVector: 'standard-compliance',
|
|
248
|
+
recommendation: 'Add return bool to function signature: function transfer(...) public returns (bool)',
|
|
249
|
+
references: [
|
|
250
|
+
'https://eips.ethereum.org/EIPS/eip-20'
|
|
251
|
+
]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate ERC721 function
|
|
258
|
+
*/
|
|
259
|
+
validateERC721Function(node, funcName, code) {
|
|
260
|
+
const codeLower = code.toLowerCase();
|
|
261
|
+
|
|
262
|
+
// transferFrom/safeTransferFrom must emit Transfer event
|
|
263
|
+
if ((funcName === 'transferFrom' || funcName === 'safeTransferFrom') &&
|
|
264
|
+
!codeLower.includes('emit transfer')) {
|
|
265
|
+
this.addFinding({
|
|
266
|
+
title: 'ERC721 Transfer Missing Transfer Event',
|
|
267
|
+
description: `ERC721 function '${funcName}' does not emit Transfer event. This violates ERC721 standard.`,
|
|
268
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
269
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
270
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
271
|
+
code: this.getCodeSnippet(node.loc),
|
|
272
|
+
severity: 'HIGH',
|
|
273
|
+
confidence: 'HIGH',
|
|
274
|
+
exploitable: false,
|
|
275
|
+
exploitabilityScore: 30,
|
|
276
|
+
attackVector: 'standard-compliance',
|
|
277
|
+
recommendation: 'Emit Transfer event: emit Transfer(from, to, tokenId);',
|
|
278
|
+
references: [
|
|
279
|
+
'https://eips.ethereum.org/EIPS/eip-721'
|
|
280
|
+
]
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// approve must emit Approval event
|
|
285
|
+
if (funcName === 'approve' && !codeLower.includes('emit approval')) {
|
|
286
|
+
this.addFinding({
|
|
287
|
+
title: 'ERC721 Approve Missing Approval Event',
|
|
288
|
+
description: `ERC721 function 'approve' does not emit Approval event. This violates ERC721 standard.`,
|
|
289
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
290
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
291
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
292
|
+
code: this.getCodeSnippet(node.loc),
|
|
293
|
+
severity: 'HIGH',
|
|
294
|
+
confidence: 'HIGH',
|
|
295
|
+
exploitable: false,
|
|
296
|
+
exploitabilityScore: 30,
|
|
297
|
+
attackVector: 'standard-compliance',
|
|
298
|
+
recommendation: 'Emit Approval event: emit Approval(owner, approved, tokenId);',
|
|
299
|
+
references: [
|
|
300
|
+
'https://eips.ethereum.org/EIPS/eip-721'
|
|
301
|
+
]
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// setApprovalForAll must emit ApprovalForAll event
|
|
306
|
+
if (funcName === 'setApprovalForAll' && !codeLower.includes('emit approvalforall')) {
|
|
307
|
+
this.addFinding({
|
|
308
|
+
title: 'ERC721 setApprovalForAll Missing ApprovalForAll Event',
|
|
309
|
+
description: `ERC721 function 'setApprovalForAll' does not emit ApprovalForAll event. This violates ERC721 standard.`,
|
|
310
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
311
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
312
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
313
|
+
code: this.getCodeSnippet(node.loc),
|
|
314
|
+
severity: 'HIGH',
|
|
315
|
+
confidence: 'HIGH',
|
|
316
|
+
exploitable: false,
|
|
317
|
+
exploitabilityScore: 30,
|
|
318
|
+
attackVector: 'standard-compliance',
|
|
319
|
+
recommendation: 'Emit ApprovalForAll event: emit ApprovalForAll(owner, operator, approved);',
|
|
320
|
+
references: [
|
|
321
|
+
'https://eips.ethereum.org/EIPS/eip-721'
|
|
322
|
+
]
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Validate ERC1155 function
|
|
329
|
+
*/
|
|
330
|
+
validateERC1155Function(node, funcName, code) {
|
|
331
|
+
const codeLower = code.toLowerCase();
|
|
332
|
+
|
|
333
|
+
// safeTransferFrom must emit TransferSingle event
|
|
334
|
+
if (funcName === 'safeTransferFrom' && !codeLower.includes('emit transfersingle')) {
|
|
335
|
+
this.addFinding({
|
|
336
|
+
title: 'ERC1155 safeTransferFrom Missing TransferSingle Event',
|
|
337
|
+
description: `ERC1155 function 'safeTransferFrom' does not emit TransferSingle event. This violates ERC1155 standard.`,
|
|
338
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
339
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
340
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
341
|
+
code: this.getCodeSnippet(node.loc),
|
|
342
|
+
severity: 'HIGH',
|
|
343
|
+
confidence: 'HIGH',
|
|
344
|
+
exploitable: false,
|
|
345
|
+
exploitabilityScore: 30,
|
|
346
|
+
attackVector: 'standard-compliance',
|
|
347
|
+
recommendation: 'Emit TransferSingle event: emit TransferSingle(operator, from, to, id, value);',
|
|
348
|
+
references: [
|
|
349
|
+
'https://eips.ethereum.org/EIPS/eip-1155'
|
|
350
|
+
]
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// safeBatchTransferFrom must emit TransferBatch event
|
|
355
|
+
if (funcName === 'safeBatchTransferFrom' && !codeLower.includes('emit transferbatch')) {
|
|
356
|
+
this.addFinding({
|
|
357
|
+
title: 'ERC1155 safeBatchTransferFrom Missing TransferBatch Event',
|
|
358
|
+
description: `ERC1155 function 'safeBatchTransferFrom' does not emit TransferBatch event. This violates ERC1155 standard.`,
|
|
359
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
360
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
361
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
362
|
+
code: this.getCodeSnippet(node.loc),
|
|
363
|
+
severity: 'HIGH',
|
|
364
|
+
confidence: 'HIGH',
|
|
365
|
+
exploitable: false,
|
|
366
|
+
exploitabilityScore: 30,
|
|
367
|
+
attackVector: 'standard-compliance',
|
|
368
|
+
recommendation: 'Emit TransferBatch event: emit TransferBatch(operator, from, to, ids, values);',
|
|
369
|
+
references: [
|
|
370
|
+
'https://eips.ethereum.org/EIPS/eip-1155'
|
|
371
|
+
]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// setApprovalForAll must emit ApprovalForAll event
|
|
376
|
+
if (funcName === 'setApprovalForAll' && !codeLower.includes('emit approvalforall')) {
|
|
377
|
+
this.addFinding({
|
|
378
|
+
title: 'ERC1155 setApprovalForAll Missing ApprovalForAll Event',
|
|
379
|
+
description: `ERC1155 function 'setApprovalForAll' does not emit ApprovalForAll event. This violates ERC1155 standard.`,
|
|
380
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
381
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
382
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
383
|
+
code: this.getCodeSnippet(node.loc),
|
|
384
|
+
severity: 'HIGH',
|
|
385
|
+
confidence: 'HIGH',
|
|
386
|
+
exploitable: false,
|
|
387
|
+
exploitabilityScore: 30,
|
|
388
|
+
attackVector: 'standard-compliance',
|
|
389
|
+
recommendation: 'Emit ApprovalForAll event: emit ApprovalForAll(owner, operator, approved);',
|
|
390
|
+
references: [
|
|
391
|
+
'https://eips.ethereum.org/EIPS/eip-1155'
|
|
392
|
+
]
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Validate event signature
|
|
399
|
+
*/
|
|
400
|
+
validateEvent(node, eventName) {
|
|
401
|
+
// Basic validation - events should have correct parameters
|
|
402
|
+
// This is a simplified check; full validation would require parameter matching
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Post-traversal compliance analysis
|
|
407
|
+
*/
|
|
408
|
+
analyzeCompliance() {
|
|
409
|
+
if (!this.tokenStandard) return;
|
|
410
|
+
|
|
411
|
+
const requiredFuncs = this.requiredFunctions[this.tokenStandard] || [];
|
|
412
|
+
const requiredEvents = this.requiredEvents[this.tokenStandard] || [];
|
|
413
|
+
|
|
414
|
+
// Check for missing required functions
|
|
415
|
+
const missingFunctions = requiredFuncs.filter(func => !this.foundFunctions.has(func));
|
|
416
|
+
if (missingFunctions.length > 0) {
|
|
417
|
+
this.addFinding({
|
|
418
|
+
title: `Missing Required ${this.tokenStandard} Functions`,
|
|
419
|
+
description: `Contract claims to implement ${this.tokenStandard} but is missing required functions: ${missingFunctions.join(', ')}. This breaks standard compliance and may cause integration issues.`,
|
|
420
|
+
location: `Contract: ${this.currentContract}`,
|
|
421
|
+
line: 1,
|
|
422
|
+
column: 0,
|
|
423
|
+
code: this.sourceCode.substring(0, 200),
|
|
424
|
+
severity: 'HIGH',
|
|
425
|
+
confidence: 'HIGH',
|
|
426
|
+
exploitable: false,
|
|
427
|
+
exploitabilityScore: 40,
|
|
428
|
+
attackVector: 'standard-compliance',
|
|
429
|
+
recommendation: `Implement all required ${this.tokenStandard} functions. Use OpenZeppelin's standard implementations as reference.`,
|
|
430
|
+
references: [
|
|
431
|
+
this.tokenStandard === 'ERC20' ? 'https://eips.ethereum.org/EIPS/eip-20' :
|
|
432
|
+
this.tokenStandard === 'ERC721' ? 'https://eips.ethereum.org/EIPS/eip-721' :
|
|
433
|
+
'https://eips.ethereum.org/EIPS/eip-1155'
|
|
434
|
+
]
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check for missing required events
|
|
439
|
+
const missingEvents = requiredEvents.filter(event => !this.foundEvents.has(event));
|
|
440
|
+
if (missingEvents.length > 0) {
|
|
441
|
+
this.addFinding({
|
|
442
|
+
title: `Missing Required ${this.tokenStandard} Events`,
|
|
443
|
+
description: `Contract claims to implement ${this.tokenStandard} but is missing required events: ${missingEvents.join(', ')}. This breaks standard compliance.`,
|
|
444
|
+
location: `Contract: ${this.currentContract}`,
|
|
445
|
+
line: 1,
|
|
446
|
+
column: 0,
|
|
447
|
+
code: this.sourceCode.substring(0, 200),
|
|
448
|
+
severity: 'HIGH',
|
|
449
|
+
confidence: 'HIGH',
|
|
450
|
+
exploitable: false,
|
|
451
|
+
exploitabilityScore: 40,
|
|
452
|
+
attackVector: 'standard-compliance',
|
|
453
|
+
recommendation: `Declare all required ${this.tokenStandard} events. Events are essential for off-chain indexing and monitoring.`,
|
|
454
|
+
references: [
|
|
455
|
+
this.tokenStandard === 'ERC20' ? 'https://eips.ethereum.org/EIPS/eip-20' :
|
|
456
|
+
this.tokenStandard === 'ERC721' ? 'https://eips.ethereum.org/EIPS/eip-721' :
|
|
457
|
+
'https://eips.ethereum.org/EIPS/eip-1155'
|
|
458
|
+
]
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = TokenStandardComplianceDetector;
|
|
465
|
+
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
class UncheckedCallDetector extends BaseDetector {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(
|
|
6
|
+
'Unchecked External Call',
|
|
7
|
+
'Detects external calls whose return values are not checked',
|
|
8
|
+
'HIGH'
|
|
9
|
+
);
|
|
10
|
+
this.potentialIssues = [];
|
|
11
|
+
this.checkedVariables = new Set();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
15
|
+
this.findings = [];
|
|
16
|
+
this.ast = ast;
|
|
17
|
+
this.sourceCode = sourceCode;
|
|
18
|
+
this.fileName = fileName;
|
|
19
|
+
this.sourceLines = sourceCode.split('\n');
|
|
20
|
+
this.cfg = cfg;
|
|
21
|
+
this.dataFlow = dataFlow;
|
|
22
|
+
this.potentialIssues = [];
|
|
23
|
+
this.checkedVariables = new Set();
|
|
24
|
+
|
|
25
|
+
// First pass: collect potential issues and checked variables
|
|
26
|
+
this.traverse(ast);
|
|
27
|
+
|
|
28
|
+
// Second pass: filter out false positives
|
|
29
|
+
this.potentialIssues = this.potentialIssues.filter(issue => {
|
|
30
|
+
return !this.checkedVariables.has(issue.variableName);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Add remaining issues as findings
|
|
34
|
+
this.potentialIssues.forEach(issue => {
|
|
35
|
+
this.addFinding(issue.finding);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return this.findings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
visitFunctionDefinition(node) {
|
|
42
|
+
// Scan entire function for all require/assert/if statements
|
|
43
|
+
if (node.body) {
|
|
44
|
+
this.scanForChecks(node.body);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
scanForChecks(node) {
|
|
49
|
+
if (!node) return;
|
|
50
|
+
|
|
51
|
+
// Check for require(success) or assert(success)
|
|
52
|
+
if (node.type === 'ExpressionStatement' && node.expression) {
|
|
53
|
+
const expr = node.expression;
|
|
54
|
+
if (expr.type === 'FunctionCall' && expr.expression) {
|
|
55
|
+
const funcName = expr.expression.name;
|
|
56
|
+
if (funcName === 'require' || funcName === 'assert') {
|
|
57
|
+
// Check first argument
|
|
58
|
+
if (expr.arguments && expr.arguments.length > 0) {
|
|
59
|
+
const arg = expr.arguments[0];
|
|
60
|
+
if (arg.type === 'Identifier') {
|
|
61
|
+
this.checkedVariables.add(arg.name);
|
|
62
|
+
}
|
|
63
|
+
// Handle !variable pattern
|
|
64
|
+
if (arg.type === 'UnaryOperation' && arg.operator === '!' && arg.subExpression && arg.subExpression.type === 'Identifier') {
|
|
65
|
+
this.checkedVariables.add(arg.subExpression.name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for if (success) or if (!success)
|
|
73
|
+
if (node.type === 'IfStatement' && node.condition) {
|
|
74
|
+
if (node.condition.type === 'Identifier') {
|
|
75
|
+
this.checkedVariables.add(node.condition.name);
|
|
76
|
+
}
|
|
77
|
+
if (node.condition.type === 'UnaryOperation' && node.condition.operator === '!' && node.condition.subExpression && node.condition.subExpression.type === 'Identifier') {
|
|
78
|
+
this.checkedVariables.add(node.condition.subExpression.name);
|
|
79
|
+
}
|
|
80
|
+
// Recursively check inside if body
|
|
81
|
+
if (node.trueBody) this.scanForChecks(node.trueBody);
|
|
82
|
+
if (node.falseBody) this.scanForChecks(node.falseBody);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Recursively check Block statements
|
|
86
|
+
if (node.type === 'Block' && node.statements) {
|
|
87
|
+
node.statements.forEach(stmt => this.scanForChecks(stmt));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check other nested structures
|
|
91
|
+
if (node.statements) {
|
|
92
|
+
node.statements.forEach(stmt => this.scanForChecks(stmt));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
visitExpressionStatement(node) {
|
|
97
|
+
if (!node.expression) return;
|
|
98
|
+
|
|
99
|
+
const expr = node.expression;
|
|
100
|
+
|
|
101
|
+
// Check for call expressions
|
|
102
|
+
if (expr.type === 'FunctionCall') {
|
|
103
|
+
this.checkFunctionCall(expr, node);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
checkFunctionCall(expr, parentNode) {
|
|
108
|
+
const code = this.getCodeSnippet(expr.loc);
|
|
109
|
+
|
|
110
|
+
// Check for low-level calls that should be checked
|
|
111
|
+
if (this.isLowLevelCall(code)) {
|
|
112
|
+
// If this is a standalone statement (not assigned or checked in if)
|
|
113
|
+
// it means the return value is ignored
|
|
114
|
+
if (parentNode.type === 'ExpressionStatement') {
|
|
115
|
+
// Determine severity based on call type
|
|
116
|
+
const isDelegatecall = code.includes('.delegatecall');
|
|
117
|
+
const hasValue = code.includes('{value:') || code.includes('{value :');
|
|
118
|
+
|
|
119
|
+
this.addFinding({
|
|
120
|
+
title: 'Unchecked Low-Level Call',
|
|
121
|
+
description: `Low-level call (${isDelegatecall ? 'delegatecall' : hasValue ? 'call with value' : 'call'}) return value is not checked. Failed calls will be silently ignored, potentially causing:\n` +
|
|
122
|
+
`- Fund loss (if sending ETH)\n` +
|
|
123
|
+
`- State inconsistency (balance decremented but transfer failed)\n` +
|
|
124
|
+
`- Silent failures in critical operations`,
|
|
125
|
+
location: this.getLocationString(expr.loc),
|
|
126
|
+
line: expr.loc ? expr.loc.start.line : 0,
|
|
127
|
+
column: expr.loc ? expr.loc.start.column : 0,
|
|
128
|
+
code: code,
|
|
129
|
+
severity: isDelegatecall ? 'CRITICAL' : 'HIGH',
|
|
130
|
+
confidence: 'HIGH', // Definite unchecked call
|
|
131
|
+
exploitable: true,
|
|
132
|
+
attackVector: 'unchecked-call',
|
|
133
|
+
recommendation: 'Always check the return value of low-level calls. Use require(success, "error message") or implement proper error handling.',
|
|
134
|
+
references: [
|
|
135
|
+
'https://swcregistry.io/docs/SWC-104',
|
|
136
|
+
'https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/'
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for .send() calls
|
|
143
|
+
if (code.includes('.send(')) {
|
|
144
|
+
if (parentNode.type === 'ExpressionStatement') {
|
|
145
|
+
this.addFinding({
|
|
146
|
+
title: 'Unchecked Send Return Value',
|
|
147
|
+
description: 'The .send() function returns false on failure, but the return value is not checked. This causes fund loss - user balance is decremented but ETH transfer fails silently.',
|
|
148
|
+
location: this.getLocationString(expr.loc),
|
|
149
|
+
line: expr.loc ? expr.loc.start.line : 0,
|
|
150
|
+
column: expr.loc ? expr.loc.start.column : 0,
|
|
151
|
+
code: code,
|
|
152
|
+
severity: 'HIGH',
|
|
153
|
+
confidence: 'HIGH', // Definite unchecked send
|
|
154
|
+
exploitable: true,
|
|
155
|
+
attackVector: 'unchecked-call',
|
|
156
|
+
recommendation: 'Check the return value: require(recipient.send(amount), "Send failed"). Consider using .transfer() which reverts on failure, or .call{value: amount}() with proper checks.',
|
|
157
|
+
references: [
|
|
158
|
+
'https://swcregistry.io/docs/SWC-104'
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
visitVariableDeclarationStatement(node) {
|
|
166
|
+
// Check if a call's return value is assigned but never used
|
|
167
|
+
if (node.variables && node.variables.length > 0) {
|
|
168
|
+
const variable = node.variables[0];
|
|
169
|
+
|
|
170
|
+
// Handle null variables (from tuple destruction like "(bool success, ) = ...")
|
|
171
|
+
if (variable && variable.name && node.initialValue) {
|
|
172
|
+
const code = this.getCodeSnippet(node.initialValue.loc);
|
|
173
|
+
|
|
174
|
+
if (this.isLowLevelCall(code)) {
|
|
175
|
+
// Check if variable name suggests it should be checked (like 'success')
|
|
176
|
+
if (variable.name.toLowerCase().includes('success')) {
|
|
177
|
+
// Store as potential issue - will be filtered later
|
|
178
|
+
this.potentialIssues.push({
|
|
179
|
+
variableName: variable.name,
|
|
180
|
+
finding: {
|
|
181
|
+
title: 'Low-Level Call Return Value Not Checked',
|
|
182
|
+
description: `Low-level call result assigned to '${variable.name}' but never validated. Failed calls will be silently ignored.`,
|
|
183
|
+
location: this.getLocationString(node.loc),
|
|
184
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
185
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
186
|
+
code: this.getCodeSnippet(node.loc),
|
|
187
|
+
recommendation: 'Add validation: require(success, "Call failed") or if (!success) revert("Call failed")',
|
|
188
|
+
references: [
|
|
189
|
+
'https://swcregistry.io/docs/SWC-104'
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
isLowLevelCall(code) {
|
|
200
|
+
return code.includes('.call(') ||
|
|
201
|
+
code.includes('.call{') ||
|
|
202
|
+
code.includes('.delegatecall(') ||
|
|
203
|
+
code.includes('.delegatecall{') ||
|
|
204
|
+
code.includes('.staticcall(') ||
|
|
205
|
+
code.includes('.staticcall{');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getLocationString(loc) {
|
|
209
|
+
if (!loc || !loc.start) return 'Unknown';
|
|
210
|
+
return `Line ${loc.start.line}, Column ${loc.start.column}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = UncheckedCallDetector;
|