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,167 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
class DelegateCallDetector extends BaseDetector {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(
|
|
6
|
+
'Dangerous Delegatecall',
|
|
7
|
+
'Detects unsafe delegatecall usage that could lead to contract takeover',
|
|
8
|
+
'CRITICAL'
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
visitFunctionCall(node) {
|
|
13
|
+
const code = this.getCodeSnippet(node.loc);
|
|
14
|
+
|
|
15
|
+
if (code.includes('delegatecall(')) {
|
|
16
|
+
this.checkDelegatecall(node, code);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
checkDelegatecall(node, code) {
|
|
21
|
+
const codeLower = code.toLowerCase();
|
|
22
|
+
|
|
23
|
+
// Skip delegatecall in fallback/receive functions with assembly (standard proxy pattern)
|
|
24
|
+
if (codeLower.includes('fallback') || codeLower.includes('receive')) {
|
|
25
|
+
if (codeLower.includes('assembly') || codeLower.includes('calldatacopy')) {
|
|
26
|
+
// This is a standard proxy pattern - check if implementation is validated
|
|
27
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
28
|
+
if (funcCode && (funcCode.includes('require') && funcCode.includes('implementation'))) {
|
|
29
|
+
return; // Secure proxy pattern
|
|
30
|
+
}
|
|
31
|
+
// Also check if it's in a function that validates implementation
|
|
32
|
+
if (this.hasWhitelistValidation(node)) {
|
|
33
|
+
return; // Has validation
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if delegatecall target is controlled by user input
|
|
39
|
+
if (this.hasUserControlledTarget(node, code)) {
|
|
40
|
+
// Check if there's whitelist validation in the function
|
|
41
|
+
if (!this.hasWhitelistValidation(node)) {
|
|
42
|
+
this.addFinding({
|
|
43
|
+
title: 'User-Controlled Delegatecall',
|
|
44
|
+
description: 'Delegatecall is made to a user-controlled address. This allows attackers to execute arbitrary code in the context of this contract, potentially taking full control.',
|
|
45
|
+
location: this.getLocationString(node.loc),
|
|
46
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
47
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
48
|
+
code: code,
|
|
49
|
+
recommendation: 'Never allow user input to control delegatecall targets. Use a whitelist of trusted contract addresses or avoid delegatecall entirely.',
|
|
50
|
+
references: [
|
|
51
|
+
'https://swcregistry.io/docs/SWC-112',
|
|
52
|
+
'https://blog.openzeppelin.com/on-the-parity-wallet-multisig-hack-405a8c12e8f7'
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if return value is checked
|
|
59
|
+
if (!this.isReturnValueChecked(node)) {
|
|
60
|
+
this.addFinding({
|
|
61
|
+
title: 'Unchecked Delegatecall Return Value',
|
|
62
|
+
description: 'Delegatecall return value is not checked. Failed delegatecalls will be silently ignored.',
|
|
63
|
+
location: this.getLocationString(node.loc),
|
|
64
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
65
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
66
|
+
code: code,
|
|
67
|
+
recommendation: 'Always check delegatecall return value: (bool success, ) = target.delegatecall(...); require(success, "Delegatecall failed");',
|
|
68
|
+
references: [
|
|
69
|
+
'https://swcregistry.io/docs/SWC-104'
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
hasWhitelistValidation(node) {
|
|
76
|
+
// Check if the function containing this delegatecall has whitelist validation
|
|
77
|
+
// Look for patterns like: require(trustedAddresses[target], ...) or require(isApproved[target], ...)
|
|
78
|
+
const lineNum = node.loc ? node.loc.start.line : 0;
|
|
79
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
80
|
+
|
|
81
|
+
// Check the function code for whitelist patterns
|
|
82
|
+
const whitelistPatterns = [
|
|
83
|
+
/require\s*\(\s*[^)]*\b(trusted|approved|whitelist|allowed|authorized)\w*\[/i,
|
|
84
|
+
/require\s*\(\s*[^)]*\b(trusted|approved|whitelist|allowed|authorized)\w*\s*\(/i,
|
|
85
|
+
/mapping\s*\([^)]*\)\s*public\s*(trusted|approved|whitelist|allowed)/i,
|
|
86
|
+
/trustedImplementations\[/i,
|
|
87
|
+
/approvedImplementations\[/i
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (whitelistPatterns.some(pattern => pattern.test(funcCode))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check a few lines before the delegatecall for validation
|
|
95
|
+
for (let i = Math.max(1, lineNum - 10); i < lineNum; i++) {
|
|
96
|
+
const line = this.getLineContent(i);
|
|
97
|
+
// Look for whitelist patterns
|
|
98
|
+
if (whitelistPatterns.some(pattern => pattern.test(line))) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
hasUserControlledTarget(node, code) {
|
|
107
|
+
// Check for common patterns of user-controlled addresses
|
|
108
|
+
if (node.expression && node.expression.type === 'MemberAccess') {
|
|
109
|
+
const target = node.expression.expression;
|
|
110
|
+
|
|
111
|
+
if (target) {
|
|
112
|
+
// Check if target is a parameter, msg.sender related, or mapping access
|
|
113
|
+
if (target.type === 'Identifier') {
|
|
114
|
+
const targetCode = this.getCodeSnippet(target.loc);
|
|
115
|
+
|
|
116
|
+
// Check if it looks like a function parameter or user-influenced variable
|
|
117
|
+
if (targetCode.includes('_') || // Common parameter naming
|
|
118
|
+
this.looksLikeFunctionParameter(targetCode)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if accessing mapping or array with user input
|
|
124
|
+
if (target.type === 'IndexAccess') {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if the delegatecall includes parameters that might be user-controlled
|
|
131
|
+
if (code.match(/delegatecall\([^)]*\b(msg\.sender|_\w+|\w+\[\w+\])/)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
looksLikeFunctionParameter(code) {
|
|
139
|
+
// Simple heuristic: parameters often start with underscore or are simple names
|
|
140
|
+
return code.startsWith('_') ||
|
|
141
|
+
code === 'target' ||
|
|
142
|
+
code === 'destination' ||
|
|
143
|
+
code === 'implementation' ||
|
|
144
|
+
code === 'logic';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
isReturnValueChecked(node) {
|
|
148
|
+
// This is a simplified check - would need parent context analysis for full accuracy
|
|
149
|
+
// We'll look for assignment to variable or usage in require/if
|
|
150
|
+
|
|
151
|
+
// Check if the node is part of an assignment
|
|
152
|
+
// This is difficult without parent reference, so we'll check the surrounding code
|
|
153
|
+
const line = this.getLineContent(node.loc ? node.loc.start.line : 0);
|
|
154
|
+
|
|
155
|
+
return line.includes('bool') ||
|
|
156
|
+
line.includes('success') ||
|
|
157
|
+
line.includes('require(') ||
|
|
158
|
+
line.includes('if (');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getLocationString(loc) {
|
|
162
|
+
if (!loc || !loc.start) return 'Unknown';
|
|
163
|
+
return `Line ${loc.start.line}, Column ${loc.start.column}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = DelegateCallDetector;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detector for deprecated functions
|
|
5
|
+
*
|
|
6
|
+
* This detector finds usage of deprecated functions like `tx.origin` for authorization,
|
|
7
|
+
* `selfdestruct` (in favor of CREATE2), and other patterns that are discouraged.
|
|
8
|
+
*/
|
|
9
|
+
class DeprecatedFunctionsDetector extends BaseDetector {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(
|
|
12
|
+
'Deprecated Functions',
|
|
13
|
+
'Finds usage of deprecated or discouraged Solidity functions and patterns',
|
|
14
|
+
'LOW' // Default severity
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
this.deprecatedPatterns = {
|
|
18
|
+
'tx.origin': {
|
|
19
|
+
severity: 'HIGH',
|
|
20
|
+
description: 'Authorization using tx.origin is insecure and can be exploited by phishing attacks.',
|
|
21
|
+
confidence: 'HIGH'
|
|
22
|
+
},
|
|
23
|
+
selfdestruct: {
|
|
24
|
+
severity: 'MEDIUM',
|
|
25
|
+
description:
|
|
26
|
+
'selfdestruct is discouraged. Consider using CREATE2 for contract removal or disabling the contract instead.',
|
|
27
|
+
confidence: 'MEDIUM'
|
|
28
|
+
},
|
|
29
|
+
'block.timestamp': {
|
|
30
|
+
severity: 'LOW',
|
|
31
|
+
description:
|
|
32
|
+
'block.timestamp can be manipulated by miners. Do not rely on it for critical logic or entropy.',
|
|
33
|
+
confidence: 'LOW'
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
visitMemberAccess(node) {
|
|
39
|
+
const expression = this.getSourceFromNode(node);
|
|
40
|
+
if (this.deprecatedPatterns[expression]) {
|
|
41
|
+
const { severity, description, confidence } = this.deprecatedPatterns[expression];
|
|
42
|
+
this.addFinding({
|
|
43
|
+
title: `Usage of deprecated or insecure pattern: ${expression}`,
|
|
44
|
+
description: description,
|
|
45
|
+
severity: severity,
|
|
46
|
+
confidence: confidence,
|
|
47
|
+
location: node.loc,
|
|
48
|
+
line: node.loc.start.line,
|
|
49
|
+
column: node.loc.start.column,
|
|
50
|
+
code: this.getCodeSnippet(node.loc)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Helper to get source from a node
|
|
56
|
+
getSourceFromNode(node) {
|
|
57
|
+
if (!node || !node.range || !this.sourceCode) return '';
|
|
58
|
+
return this.sourceCode.substring(node.range[0], node.range[1] + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = DeprecatedFunctionsDetector;
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flash Loan Attack Pattern Detector (Enhanced)
|
|
5
|
+
* Detects patterns vulnerable to flash loan manipulation with context awareness
|
|
6
|
+
* to reduce false positives and classify real exploitable issues.
|
|
7
|
+
*/
|
|
8
|
+
class FlashLoanDetector extends BaseDetector {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(
|
|
11
|
+
'Flash Loan Vulnerability',
|
|
12
|
+
'Detects patterns vulnerable to flash loan attacks',
|
|
13
|
+
'CRITICAL'
|
|
14
|
+
);
|
|
15
|
+
this.currentContract = null;
|
|
16
|
+
this.currentFunction = null;
|
|
17
|
+
this.currentFunctionNode = null;
|
|
18
|
+
this.cfg = null;
|
|
19
|
+
this.balanceUsages = [];
|
|
20
|
+
this.priceCalculations = [];
|
|
21
|
+
this.valueFlows = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
25
|
+
this.findings = [];
|
|
26
|
+
this.ast = ast;
|
|
27
|
+
this.sourceCode = sourceCode;
|
|
28
|
+
this.fileName = fileName;
|
|
29
|
+
this.sourceLines = sourceCode.split('\n');
|
|
30
|
+
this.cfg = cfg;
|
|
31
|
+
this.dataFlow = dataFlow;
|
|
32
|
+
|
|
33
|
+
this.traverse(ast);
|
|
34
|
+
|
|
35
|
+
return this.findings;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
visitContractDefinition(node) {
|
|
39
|
+
this.currentContract = node.name;
|
|
40
|
+
// Reset per-contract tracking
|
|
41
|
+
this.balanceUsages = [];
|
|
42
|
+
this.priceCalculations = [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
visitFunctionDefinition(node) {
|
|
46
|
+
this.currentFunction = node.name || 'constructor';
|
|
47
|
+
this.currentFunctionNode = node;
|
|
48
|
+
|
|
49
|
+
// Reset per-function tracking
|
|
50
|
+
this.functionBalanceUsages = [];
|
|
51
|
+
this.functionDivisions = [];
|
|
52
|
+
this.functionExternalCalls = [];
|
|
53
|
+
|
|
54
|
+
// Analyze the full function for flash loan patterns
|
|
55
|
+
if (node.body) {
|
|
56
|
+
this.deepAnalyzeFunction(node);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deep analysis of function to find actual exploitable flash loan patterns
|
|
62
|
+
* Only flags when: balance/reserves → division/calculation → value transfer/minting
|
|
63
|
+
*/
|
|
64
|
+
deepAnalyzeFunction(funcNode) {
|
|
65
|
+
const funcCode = this.getCodeSnippet(funcNode.loc);
|
|
66
|
+
const funcCodeLower = funcCode.toLowerCase();
|
|
67
|
+
|
|
68
|
+
// Skip internal/private functions (not directly exploitable)
|
|
69
|
+
if (funcNode.visibility === 'private' || funcNode.visibility === 'internal') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skip view/pure functions (can't cause direct fund loss)
|
|
74
|
+
if (funcNode.stateMutability === 'view' || funcNode.stateMutability === 'pure') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for known safe patterns that should be excluded
|
|
79
|
+
if (this.hasSafeGuards(funcCode)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Pattern 1: Spot price oracle for value calculations (HIGH confidence)
|
|
84
|
+
const spotPricePattern = this.detectSpotPriceOracle(funcCode, funcNode);
|
|
85
|
+
|
|
86
|
+
// Pattern 2: Balance-based pricing (MEDIUM-HIGH confidence depending on context)
|
|
87
|
+
const balancePricingPattern = this.detectBalanceBasedPricing(funcCode, funcNode);
|
|
88
|
+
|
|
89
|
+
// Pattern 3: Reserve ratio manipulation (HIGH confidence)
|
|
90
|
+
const reserveManipPattern = this.detectReserveManipulation(funcCode, funcNode);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check for safe guards that mitigate flash loan attacks
|
|
95
|
+
*/
|
|
96
|
+
hasSafeGuards(code) {
|
|
97
|
+
const safePatterns = [
|
|
98
|
+
// TWAP oracles
|
|
99
|
+
/observe\s*\(/i,
|
|
100
|
+
/consult\s*\(/i,
|
|
101
|
+
/twap/i,
|
|
102
|
+
/timeWeightedAverage/i,
|
|
103
|
+
// Chainlink oracles
|
|
104
|
+
/latestRoundData/i,
|
|
105
|
+
/priceFeed/i,
|
|
106
|
+
/AggregatorV3/i,
|
|
107
|
+
// Multi-block checks
|
|
108
|
+
/block\.number\s*-/i,
|
|
109
|
+
/previousBlock/i,
|
|
110
|
+
/lastUpdateBlock/i,
|
|
111
|
+
// Flash loan callbacks that indicate awareness
|
|
112
|
+
/onFlashLoan/i,
|
|
113
|
+
/flashLoanCallback/i,
|
|
114
|
+
// Internal accounting (safe pattern)
|
|
115
|
+
/internalBalance/i,
|
|
116
|
+
/_balance\[/i,
|
|
117
|
+
/balances\[/i,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return safePatterns.some(pattern => pattern.test(code));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Detect spot price oracle usage that flows into value calculations
|
|
125
|
+
*/
|
|
126
|
+
detectSpotPriceOracle(code, funcNode) {
|
|
127
|
+
// High-risk spot price functions
|
|
128
|
+
// Note: Don't use /g flag with test() as it causes stateful behavior
|
|
129
|
+
const spotPricePatterns = [
|
|
130
|
+
{ pattern: /\.getReserves\s*\(\s*\)/, name: 'getReserves' },
|
|
131
|
+
{ pattern: /\.getAmountsOut\s*\(/, name: 'getAmountsOut' },
|
|
132
|
+
{ pattern: /\.getAmountOut\s*\(/, name: 'getAmountOut' },
|
|
133
|
+
{ pattern: /\.getAmountsIn\s*\(/, name: 'getAmountsIn' },
|
|
134
|
+
{ pattern: /\.quote\s*\(/, name: 'quote' },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
for (const { pattern, name } of spotPricePatterns) {
|
|
138
|
+
if (pattern.test(code)) {
|
|
139
|
+
// Check if result flows into value-affecting operations
|
|
140
|
+
const flowsToValue = this.checkValueFlow(code, name);
|
|
141
|
+
|
|
142
|
+
if (flowsToValue.isExploitable) {
|
|
143
|
+
this.addFinding({
|
|
144
|
+
title: 'Spot Price Oracle Manipulation',
|
|
145
|
+
description: `Function uses '${name}()' for price calculation which can be manipulated in a single transaction via flash loans. ${flowsToValue.reason}`,
|
|
146
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
147
|
+
line: funcNode.loc ? funcNode.loc.start.line : 0,
|
|
148
|
+
column: funcNode.loc ? funcNode.loc.start.column : 0,
|
|
149
|
+
code: this.extractRelevantCode(code, name),
|
|
150
|
+
severity: 'CRITICAL',
|
|
151
|
+
confidence: 'HIGH',
|
|
152
|
+
exploitable: true,
|
|
153
|
+
exploitabilityScore: 85,
|
|
154
|
+
attackVector: 'flash-loan-oracle-manipulation',
|
|
155
|
+
recommendation: 'Replace spot price oracle with TWAP oracle (Uniswap V3 observe()) or Chainlink price feeds (latestRoundData). Never use single-block prices for value calculations.',
|
|
156
|
+
references: [
|
|
157
|
+
'https://docs.chain.link/data-feeds',
|
|
158
|
+
'https://docs.uniswap.org/concepts/protocol/oracle',
|
|
159
|
+
'https://www.paradigm.xyz/2020/11/so-you-want-to-use-a-price-oracle'
|
|
160
|
+
],
|
|
161
|
+
foundryPoC: this.generateSpotPricePoC(name, this.currentContract, this.currentFunction)
|
|
162
|
+
});
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detect balance-based pricing that could be manipulated
|
|
172
|
+
*/
|
|
173
|
+
detectBalanceBasedPricing(code, funcNode) {
|
|
174
|
+
// Only flag if balance is used in division for pricing
|
|
175
|
+
const balanceInDivision = /(?:balanceOf|\.balance)[^;]*\/[^;]*|[^;]*\/[^;]*(?:balanceOf|\.balance)/gi;
|
|
176
|
+
|
|
177
|
+
if (!balanceInDivision.test(code)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if this affects value transfers or minting
|
|
182
|
+
const valueAffecting = [
|
|
183
|
+
/\.transfer\s*\(/i,
|
|
184
|
+
/\.call\s*\{.*value/i,
|
|
185
|
+
/\.mint\s*\(/i,
|
|
186
|
+
/\.burn\s*\(/i,
|
|
187
|
+
/safeTransfer/i,
|
|
188
|
+
/_mint\s*\(/i,
|
|
189
|
+
/_burn\s*\(/i,
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const affectsValue = valueAffecting.some(pattern => pattern.test(code));
|
|
193
|
+
|
|
194
|
+
if (!affectsValue) {
|
|
195
|
+
// Balance used but doesn't affect value - likely internal accounting
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check for price/rate/ratio context
|
|
200
|
+
const pricingContext = /(?:price|rate|ratio|exchange|collateral|liquidat)/i.test(code);
|
|
201
|
+
|
|
202
|
+
if (pricingContext) {
|
|
203
|
+
this.addFinding({
|
|
204
|
+
title: 'Balance-Based Pricing Vulnerable to Flash Loan',
|
|
205
|
+
description: `Function calculates price/rate using real-time balance which can be manipulated via flash loans, donations, or selfdestruct. The calculated value affects token transfers or minting.`,
|
|
206
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
207
|
+
line: funcNode.loc ? funcNode.loc.start.line : 0,
|
|
208
|
+
column: funcNode.loc ? funcNode.loc.start.column : 0,
|
|
209
|
+
code: this.extractRelevantCode(code, 'balance'),
|
|
210
|
+
severity: 'HIGH',
|
|
211
|
+
confidence: 'HIGH',
|
|
212
|
+
exploitable: true,
|
|
213
|
+
exploitabilityScore: 75,
|
|
214
|
+
attackVector: 'flash-loan-balance-manipulation',
|
|
215
|
+
recommendation: 'Use internal balance tracking instead of actual balances. Implement deposit/withdraw pattern with state variables. Consider using TWAP for any pricing logic.',
|
|
216
|
+
references: [
|
|
217
|
+
'https://swcregistry.io/docs/SWC-132',
|
|
218
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/oracle-manipulation/'
|
|
219
|
+
],
|
|
220
|
+
foundryPoC: this.generateBalanceManipulationPoC(this.currentContract, this.currentFunction)
|
|
221
|
+
});
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Detect reserve ratio manipulation patterns
|
|
230
|
+
*/
|
|
231
|
+
detectReserveManipulation(code, funcNode) {
|
|
232
|
+
// Pattern: reserve0 / reserve1 or similar calculations
|
|
233
|
+
const reserveRatio = /reserve[0-9]?\s*[*/]\s*reserve[0-9]?/gi;
|
|
234
|
+
|
|
235
|
+
if (!reserveRatio.test(code)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for swap or exchange context
|
|
240
|
+
if (/swap|exchange|trade/i.test(code)) {
|
|
241
|
+
this.addFinding({
|
|
242
|
+
title: 'Reserve Ratio Manipulation Risk',
|
|
243
|
+
description: `Function uses reserve ratio calculation that can be manipulated within a single transaction. Attackers can inflate/deflate reserves using flash loans before this calculation.`,
|
|
244
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
245
|
+
line: funcNode.loc ? funcNode.loc.start.line : 0,
|
|
246
|
+
column: funcNode.loc ? funcNode.loc.start.column : 0,
|
|
247
|
+
code: this.extractRelevantCode(code, 'reserve'),
|
|
248
|
+
severity: 'HIGH',
|
|
249
|
+
confidence: 'HIGH',
|
|
250
|
+
exploitable: true,
|
|
251
|
+
exploitabilityScore: 80,
|
|
252
|
+
attackVector: 'flash-loan-reserve-manipulation',
|
|
253
|
+
recommendation: 'Use TWAP pricing instead of spot reserve ratios. Implement slippage protection. Consider using Chainlink oracles for critical price data.',
|
|
254
|
+
references: [
|
|
255
|
+
'https://www.paradigm.xyz/2020/11/so-you-want-to-use-a-price-oracle'
|
|
256
|
+
]
|
|
257
|
+
});
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if spot price result flows into value-affecting operations
|
|
266
|
+
*/
|
|
267
|
+
checkValueFlow(code, priceFunctionName) {
|
|
268
|
+
// Check what happens after the price call
|
|
269
|
+
const valueOps = [
|
|
270
|
+
{ pattern: /\.transfer\s*\(/i, reason: 'Price feeds into ETH/token transfer amount' },
|
|
271
|
+
{ pattern: /\.call\s*\{.*value/i, reason: 'Price feeds into call value' },
|
|
272
|
+
{ pattern: /\.mint\s*\(/i, reason: 'Price determines mint amount' },
|
|
273
|
+
{ pattern: /\.burn\s*\(/i, reason: 'Price determines burn amount' },
|
|
274
|
+
{ pattern: /safeTransfer/i, reason: 'Price determines transfer amount' },
|
|
275
|
+
{ pattern: /collateral/i, reason: 'Price used for collateral valuation' },
|
|
276
|
+
{ pattern: /liquidat/i, reason: 'Price triggers liquidation' },
|
|
277
|
+
{ pattern: /borrow/i, reason: 'Price determines borrow capacity' },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const { pattern, reason } of valueOps) {
|
|
281
|
+
if (pattern.test(code)) {
|
|
282
|
+
return { isExploitable: true, reason };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { isExploitable: false, reason: '' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Extract the most relevant code snippet around a pattern
|
|
291
|
+
*/
|
|
292
|
+
extractRelevantCode(fullCode, keyword) {
|
|
293
|
+
const lines = fullCode.split('\n');
|
|
294
|
+
const keywordLower = keyword.toLowerCase();
|
|
295
|
+
|
|
296
|
+
let relevantLines = [];
|
|
297
|
+
let foundLine = -1;
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < lines.length; i++) {
|
|
300
|
+
if (lines[i].toLowerCase().includes(keywordLower)) {
|
|
301
|
+
foundLine = i;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (foundLine >= 0) {
|
|
307
|
+
const start = Math.max(0, foundLine - 2);
|
|
308
|
+
const end = Math.min(lines.length, foundLine + 3);
|
|
309
|
+
relevantLines = lines.slice(start, end);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return relevantLines.join('\n') || fullCode.substring(0, 200);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
visitMemberAccess(node) {
|
|
316
|
+
// Detect spot price oracle usage at AST level for precise location
|
|
317
|
+
if (node.memberName === 'getReserves' ||
|
|
318
|
+
node.memberName === 'getAmountsOut' ||
|
|
319
|
+
node.memberName === 'getAmountOut') {
|
|
320
|
+
// Already handled in deepAnalyzeFunction with context
|
|
321
|
+
// This is kept for precise line number tracking
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generate Foundry PoC for spot price manipulation
|
|
327
|
+
*/
|
|
328
|
+
generateSpotPricePoC(oracleFunction, contractName, functionName) {
|
|
329
|
+
return `// SPDX-License-Identifier: MIT
|
|
330
|
+
pragma solidity ^0.8.0;
|
|
331
|
+
|
|
332
|
+
import "forge-std/Test.sol";
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Proof of Concept: Flash Loan Oracle Manipulation
|
|
336
|
+
* Target: ${contractName}.${functionName}()
|
|
337
|
+
* Attack Vector: Manipulate ${oracleFunction}() return value via flash loan
|
|
338
|
+
*/
|
|
339
|
+
contract FlashLoanOracleExploit is Test {
|
|
340
|
+
// TODO: Set actual addresses
|
|
341
|
+
address constant TARGET = address(0); // ${contractName} address
|
|
342
|
+
address constant DEX_PAIR = address(0); // Uniswap/DEX pair for manipulation
|
|
343
|
+
address constant FLASH_LOAN_PROVIDER = address(0); // Aave/dYdX
|
|
344
|
+
|
|
345
|
+
function testExploit() public {
|
|
346
|
+
// 1. Take flash loan to get large amount of tokens
|
|
347
|
+
// flashLoan(FLASH_LOAN_PROVIDER, amount);
|
|
348
|
+
|
|
349
|
+
// 2. Manipulate DEX reserves to skew ${oracleFunction}()
|
|
350
|
+
// swap large amount to move price
|
|
351
|
+
|
|
352
|
+
// 3. Call vulnerable function while price is manipulated
|
|
353
|
+
// TARGET.${functionName}(...);
|
|
354
|
+
|
|
355
|
+
// 4. Reverse the manipulation
|
|
356
|
+
// swap back to restore price
|
|
357
|
+
|
|
358
|
+
// 5. Repay flash loan, keep profit
|
|
359
|
+
|
|
360
|
+
// Assert profit was made
|
|
361
|
+
// assertGt(profit, 0);
|
|
362
|
+
}
|
|
363
|
+
}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Generate Foundry PoC for balance manipulation
|
|
368
|
+
*/
|
|
369
|
+
generateBalanceManipulationPoC(contractName, functionName) {
|
|
370
|
+
return `// SPDX-License-Identifier: MIT
|
|
371
|
+
pragma solidity ^0.8.0;
|
|
372
|
+
|
|
373
|
+
import "forge-std/Test.sol";
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Proof of Concept: Balance Manipulation Attack
|
|
377
|
+
* Target: ${contractName}.${functionName}()
|
|
378
|
+
* Attack Vector: Manipulate contract balance before price calculation
|
|
379
|
+
*/
|
|
380
|
+
contract BalanceManipulationExploit is Test {
|
|
381
|
+
address constant TARGET = address(0); // ${contractName} address
|
|
382
|
+
|
|
383
|
+
function testExploit() public {
|
|
384
|
+
// Method 1: Flash loan + donation
|
|
385
|
+
// 1. Take flash loan
|
|
386
|
+
// 2. Send tokens to target contract (donation)
|
|
387
|
+
// 3. Call vulnerable function (inflated balance = wrong price)
|
|
388
|
+
// 4. Extract value at manipulated price
|
|
389
|
+
// 5. Repay flash loan
|
|
390
|
+
|
|
391
|
+
// Method 2: Self-destruct (for ETH balance)
|
|
392
|
+
// Deploy contract with ETH, selfdestruct to target
|
|
393
|
+
|
|
394
|
+
// Assert profit was made
|
|
395
|
+
// assertGt(profit, 0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Helper: Self-destruct ETH sender
|
|
399
|
+
// contract Depositor {
|
|
400
|
+
// constructor(address target) payable {
|
|
401
|
+
// selfdestruct(payable(target));
|
|
402
|
+
// }
|
|
403
|
+
// }
|
|
404
|
+
}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = FlashLoanDetector;
|