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,408 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Precision Loss Detector
|
|
5
|
+
* Detects mathematical operations that cause precision loss:
|
|
6
|
+
* - Division before multiplication (truncation)
|
|
7
|
+
* - Rounding errors in financial calculations
|
|
8
|
+
* - Loss of precision in decimal conversions
|
|
9
|
+
*
|
|
10
|
+
* Classification:
|
|
11
|
+
* - CRITICAL: Division before multiplication in token/price calculations
|
|
12
|
+
* - HIGH: Rounding errors affecting user balances
|
|
13
|
+
* - MEDIUM: Precision loss in non-financial contexts
|
|
14
|
+
*/
|
|
15
|
+
class PrecisionLossDetector extends BaseDetector {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(
|
|
18
|
+
'Precision Loss',
|
|
19
|
+
'Detects mathematical operations causing precision loss',
|
|
20
|
+
'HIGH'
|
|
21
|
+
);
|
|
22
|
+
this.currentContract = null;
|
|
23
|
+
this.currentFunction = null;
|
|
24
|
+
this.currentFunctionNode = null;
|
|
25
|
+
this.variableTypes = new Map();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
29
|
+
this.findings = [];
|
|
30
|
+
this.ast = ast;
|
|
31
|
+
this.sourceCode = sourceCode;
|
|
32
|
+
this.fileName = fileName;
|
|
33
|
+
this.sourceLines = sourceCode.split('\n');
|
|
34
|
+
this.cfg = cfg;
|
|
35
|
+
this.dataFlow = dataFlow;
|
|
36
|
+
|
|
37
|
+
this.traverse(ast);
|
|
38
|
+
|
|
39
|
+
return this.findings;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
visitContractDefinition(node) {
|
|
43
|
+
this.currentContract = node.name;
|
|
44
|
+
this.variableTypes.clear();
|
|
45
|
+
|
|
46
|
+
// Track state variable types
|
|
47
|
+
if (node.subNodes) {
|
|
48
|
+
for (const subNode of node.subNodes) {
|
|
49
|
+
if (subNode.type === 'StateVariableDeclaration') {
|
|
50
|
+
this.trackVariableType(subNode);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
visitFunctionDefinition(node) {
|
|
57
|
+
this.currentFunction = node.name || 'constructor';
|
|
58
|
+
this.currentFunctionNode = node;
|
|
59
|
+
|
|
60
|
+
// Skip view/pure functions for critical financial issues (they don't modify state)
|
|
61
|
+
// but still check for precision issues
|
|
62
|
+
if (node.body) {
|
|
63
|
+
this.analyzeFunction(node);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
trackVariableType(node) {
|
|
68
|
+
if (node.variables) {
|
|
69
|
+
for (const variable of node.variables) {
|
|
70
|
+
if (variable.name && variable.typeName) {
|
|
71
|
+
this.variableTypes.set(variable.name, this.getTypeName(variable.typeName));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getTypeName(typeNode) {
|
|
78
|
+
if (!typeNode) return 'unknown';
|
|
79
|
+
if (typeNode.type === 'ElementaryTypeName') {
|
|
80
|
+
return typeNode.name;
|
|
81
|
+
}
|
|
82
|
+
if (typeNode.type === 'UserDefinedTypeName') {
|
|
83
|
+
return typeNode.namePath;
|
|
84
|
+
}
|
|
85
|
+
if (typeNode.type === 'ArrayTypeName') {
|
|
86
|
+
return this.getTypeName(typeNode.baseTypeName) + '[]';
|
|
87
|
+
}
|
|
88
|
+
return 'unknown';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
analyzeFunction(funcNode) {
|
|
92
|
+
const funcCode = this.getCodeSnippet(funcNode.loc);
|
|
93
|
+
|
|
94
|
+
// Pattern 1: Division before multiplication (most common precision loss)
|
|
95
|
+
this.detectDivisionBeforeMultiplication(funcNode, funcCode);
|
|
96
|
+
|
|
97
|
+
// Pattern 2: Direct division without scaling
|
|
98
|
+
this.detectUnscaledDivision(funcNode, funcCode);
|
|
99
|
+
|
|
100
|
+
// Pattern 3: Decimal conversion issues
|
|
101
|
+
this.detectDecimalConversionIssues(funcNode, funcCode);
|
|
102
|
+
|
|
103
|
+
// Pattern 4: Percentage calculations losing precision
|
|
104
|
+
this.detectPercentageIssues(funcNode, funcCode);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Detect division before multiplication pattern
|
|
109
|
+
* Example: a / b * c should be a * c / b
|
|
110
|
+
*/
|
|
111
|
+
detectDivisionBeforeMultiplication(funcNode, funcCode) {
|
|
112
|
+
// Pattern: expression / value * anotherValue
|
|
113
|
+
// This truncates the intermediate result before multiplication
|
|
114
|
+
const divBeforeMulPattern = /(\w+)\s*\/\s*(\w+)\s*\*\s*(\w+)/g;
|
|
115
|
+
let match;
|
|
116
|
+
|
|
117
|
+
while ((match = divBeforeMulPattern.exec(funcCode)) !== null) {
|
|
118
|
+
const [fullMatch, dividend, divisor, multiplier] = match;
|
|
119
|
+
|
|
120
|
+
// Check if this is in a financial context
|
|
121
|
+
const isFinancial = this.isFinancialContext(funcCode, match.index);
|
|
122
|
+
|
|
123
|
+
// Skip if using safe multiplication patterns
|
|
124
|
+
if (this.usesSafeMathPattern(funcCode)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Calculate the line number
|
|
129
|
+
const linesBefore = funcCode.substring(0, match.index).split('\n').length;
|
|
130
|
+
const line = (funcNode.loc?.start?.line || 0) + linesBefore - 1;
|
|
131
|
+
|
|
132
|
+
this.addFinding({
|
|
133
|
+
title: 'Division Before Multiplication',
|
|
134
|
+
description: `Expression '${fullMatch}' performs division before multiplication, causing precision loss due to integer truncation. In Solidity, (a/b)*c loses precision compared to (a*c)/b.`,
|
|
135
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
136
|
+
line: line,
|
|
137
|
+
column: 0,
|
|
138
|
+
code: this.extractContext(funcCode, match.index),
|
|
139
|
+
severity: isFinancial ? 'CRITICAL' : 'HIGH',
|
|
140
|
+
confidence: 'HIGH',
|
|
141
|
+
exploitable: isFinancial,
|
|
142
|
+
exploitabilityScore: isFinancial ? 85 : 65,
|
|
143
|
+
attackVector: 'precision-loss',
|
|
144
|
+
recommendation: `Reorder to multiply before dividing: (${dividend} * ${multiplier}) / ${divisor}. For percentage calculations, multiply by the percentage first, then divide by 100 or the base.`,
|
|
145
|
+
references: [
|
|
146
|
+
'https://github.com/crytic/slither/wiki/Detector-Documentation#divide-before-multiply',
|
|
147
|
+
'https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/integer-division/'
|
|
148
|
+
],
|
|
149
|
+
foundryPoC: isFinancial ? this.generateDivBeforeMulPoC(dividend, divisor, multiplier) : undefined
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Detect division without proper scaling
|
|
156
|
+
* Example: amount / totalSupply without WAD/RAY scaling
|
|
157
|
+
*/
|
|
158
|
+
detectUnscaledDivision(funcNode, funcCode) {
|
|
159
|
+
// Look for ratio calculations without scaling
|
|
160
|
+
const ratioPatterns = [
|
|
161
|
+
{ pattern: /(\w+)\s*\/\s*totalSupply/gi, context: 'share calculation' },
|
|
162
|
+
{ pattern: /(\w+)\s*\/\s*(\w+Supply)/gi, context: 'supply ratio' },
|
|
163
|
+
{ pattern: /amount\s*\/\s*(\w+)/gi, context: 'amount division' },
|
|
164
|
+
{ pattern: /balance\s*\/\s*(\w+)/gi, context: 'balance division' },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
for (const { pattern, context } of ratioPatterns) {
|
|
168
|
+
let match;
|
|
169
|
+
pattern.lastIndex = 0; // Reset regex state
|
|
170
|
+
|
|
171
|
+
while ((match = pattern.exec(funcCode)) !== null) {
|
|
172
|
+
// Check if there's scaling (WAD, RAY, 1e18, etc.)
|
|
173
|
+
const hasScaling = this.hasProperScaling(funcCode, match.index);
|
|
174
|
+
|
|
175
|
+
if (!hasScaling && !this.isSafeContext(funcCode, match.index)) {
|
|
176
|
+
const linesBefore = funcCode.substring(0, match.index).split('\n').length;
|
|
177
|
+
const line = (funcNode.loc?.start?.line || 0) + linesBefore - 1;
|
|
178
|
+
|
|
179
|
+
this.addFinding({
|
|
180
|
+
title: 'Unscaled Division May Lose Precision',
|
|
181
|
+
description: `Division in ${context} without precision scaling. When dividing integers, small amounts may round to zero. Consider using WAD (1e18) or RAY (1e27) scaling.`,
|
|
182
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
183
|
+
line: line,
|
|
184
|
+
column: 0,
|
|
185
|
+
code: this.extractContext(funcCode, match.index),
|
|
186
|
+
severity: 'MEDIUM',
|
|
187
|
+
confidence: 'MEDIUM',
|
|
188
|
+
exploitable: true,
|
|
189
|
+
exploitabilityScore: 55,
|
|
190
|
+
attackVector: 'precision-loss',
|
|
191
|
+
recommendation: 'Scale the numerator before division: (amount * 1e18) / totalSupply. Use fixed-point math libraries like DSMath or PRBMath for precise calculations.',
|
|
192
|
+
references: [
|
|
193
|
+
'https://github.com/dapphub/ds-math',
|
|
194
|
+
'https://github.com/PaulRBerg/prb-math'
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Detect decimal conversion issues between tokens with different decimals
|
|
204
|
+
*/
|
|
205
|
+
detectDecimalConversionIssues(funcNode, funcCode) {
|
|
206
|
+
// Check for operations mixing decimals
|
|
207
|
+
const decimalPatterns = [
|
|
208
|
+
/decimals\s*\(\s*\)/gi,
|
|
209
|
+
/10\s*\*\*\s*(\w+)/gi,
|
|
210
|
+
/1e(\d+)/gi,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const hasDecimalHandling = decimalPatterns.some(p => p.test(funcCode));
|
|
214
|
+
|
|
215
|
+
// If function deals with multiple tokens and doesn't handle decimals properly
|
|
216
|
+
if (funcCode.includes('token') && funcCode.includes('/')) {
|
|
217
|
+
const multiTokenPattern = /token[A-Z0-9]?\.balanceOf|IERC20\([^)]+\)\.balanceOf/gi;
|
|
218
|
+
const tokenMatches = funcCode.match(multiTokenPattern);
|
|
219
|
+
|
|
220
|
+
if (tokenMatches && tokenMatches.length > 1 && !hasDecimalHandling) {
|
|
221
|
+
this.addFinding({
|
|
222
|
+
title: 'Missing Decimal Normalization',
|
|
223
|
+
description: `Function operates on multiple tokens without decimal normalization. Tokens with different decimals (e.g., USDC=6, DAI=18) will produce incorrect calculations.`,
|
|
224
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
225
|
+
line: funcNode.loc?.start?.line || 0,
|
|
226
|
+
column: 0,
|
|
227
|
+
code: this.extractContext(funcCode, 0),
|
|
228
|
+
severity: 'HIGH',
|
|
229
|
+
confidence: 'MEDIUM',
|
|
230
|
+
exploitable: true,
|
|
231
|
+
exploitabilityScore: 70,
|
|
232
|
+
attackVector: 'precision-loss',
|
|
233
|
+
recommendation: 'Normalize all token amounts to a common decimal base (typically 18 decimals) before performing calculations. Use: normalizedAmount = amount * (10 ** (18 - tokenDecimals)).',
|
|
234
|
+
references: [
|
|
235
|
+
'https://consensys.github.io/smart-contract-best-practices/development-recommendations/token-specific/token-normalization/'
|
|
236
|
+
]
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Detect percentage calculation issues
|
|
244
|
+
*/
|
|
245
|
+
detectPercentageIssues(funcNode, funcCode) {
|
|
246
|
+
// Common problematic percentage patterns
|
|
247
|
+
const percentPatterns = [
|
|
248
|
+
// amount * percentage / 100 where percentage might be small
|
|
249
|
+
{ pattern: /(\w+)\s*\*\s*(\w+)\s*\/\s*100\b/gi, risk: 'low percentage precision' },
|
|
250
|
+
// Basis points without proper handling
|
|
251
|
+
{ pattern: /(\w+)\s*\*\s*(\w+)\s*\/\s*10000\b/gi, risk: 'basis points precision' },
|
|
252
|
+
// Fee calculation that might round to zero
|
|
253
|
+
{ pattern: /fee\s*=\s*(\w+)\s*\*\s*(\w+)\s*\/\s*(\w+)/gi, risk: 'fee rounding' },
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
for (const { pattern, risk } of percentPatterns) {
|
|
257
|
+
pattern.lastIndex = 0;
|
|
258
|
+
let match;
|
|
259
|
+
|
|
260
|
+
while ((match = pattern.exec(funcCode)) !== null) {
|
|
261
|
+
// Check if there's a minimum check or rounding protection
|
|
262
|
+
const hasMinCheck = /require\s*\([^)]*>\s*0|Math\.max/i.test(funcCode);
|
|
263
|
+
|
|
264
|
+
if (!hasMinCheck) {
|
|
265
|
+
const linesBefore = funcCode.substring(0, match.index).split('\n').length;
|
|
266
|
+
const line = (funcNode.loc?.start?.line || 0) + linesBefore - 1;
|
|
267
|
+
|
|
268
|
+
this.addFinding({
|
|
269
|
+
title: 'Percentage Calculation May Round to Zero',
|
|
270
|
+
description: `Percentage calculation (${risk}) may produce zero for small amounts. Without minimum value protection, users could receive zero fees/rewards or bypass fee payments.`,
|
|
271
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
272
|
+
line: line,
|
|
273
|
+
column: 0,
|
|
274
|
+
code: this.extractContext(funcCode, match.index),
|
|
275
|
+
severity: 'MEDIUM',
|
|
276
|
+
confidence: 'MEDIUM',
|
|
277
|
+
exploitable: true,
|
|
278
|
+
exploitabilityScore: 50,
|
|
279
|
+
attackVector: 'precision-loss',
|
|
280
|
+
recommendation: 'Add minimum value check: require(result > 0) or use Math.max(1, result). For fees, consider a minimum fee constant.',
|
|
281
|
+
references: [
|
|
282
|
+
'https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/integer-division/'
|
|
283
|
+
]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check if context involves financial calculations
|
|
292
|
+
*/
|
|
293
|
+
isFinancialContext(code, position) {
|
|
294
|
+
const context = code.substring(Math.max(0, position - 100), position + 100).toLowerCase();
|
|
295
|
+
const financialKeywords = [
|
|
296
|
+
'balance', 'amount', 'price', 'rate', 'fee', 'reward', 'stake',
|
|
297
|
+
'yield', 'interest', 'collateral', 'debt', 'liquidity', 'share',
|
|
298
|
+
'token', 'mint', 'burn', 'transfer', 'swap', 'exchange'
|
|
299
|
+
];
|
|
300
|
+
return financialKeywords.some(kw => context.includes(kw));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if code uses SafeMath or similar patterns
|
|
305
|
+
*/
|
|
306
|
+
usesSafeMathPattern(code) {
|
|
307
|
+
return /\.mul\(|\.div\(|mulDiv|FullMath|PRBMath|DSMath/i.test(code);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check if division has proper scaling
|
|
312
|
+
*/
|
|
313
|
+
hasProperScaling(code, position) {
|
|
314
|
+
const context = code.substring(Math.max(0, position - 50), Math.min(code.length, position + 100));
|
|
315
|
+
return /1e18|1e27|WAD|RAY|PRECISION|SCALE|10\s*\*\*\s*(18|27)/i.test(context);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if context is safe (e.g., already validated)
|
|
320
|
+
*/
|
|
321
|
+
isSafeContext(code, position) {
|
|
322
|
+
const context = code.substring(Math.max(0, position - 100), position);
|
|
323
|
+
// Check for prior validation
|
|
324
|
+
return /require\s*\([^)]*>\s*0|if\s*\([^)]*>\s*0/i.test(context);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Extract relevant code context around a position
|
|
329
|
+
*/
|
|
330
|
+
extractContext(code, position) {
|
|
331
|
+
const lines = code.split('\n');
|
|
332
|
+
const linesBefore = code.substring(0, position).split('\n').length - 1;
|
|
333
|
+
|
|
334
|
+
const start = Math.max(0, linesBefore - 1);
|
|
335
|
+
const end = Math.min(lines.length, linesBefore + 3);
|
|
336
|
+
|
|
337
|
+
return lines.slice(start, end).join('\n');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Generate Foundry PoC for division before multiplication
|
|
342
|
+
*/
|
|
343
|
+
generateDivBeforeMulPoC(dividend, divisor, multiplier) {
|
|
344
|
+
return `// SPDX-License-Identifier: MIT
|
|
345
|
+
pragma solidity ^0.8.0;
|
|
346
|
+
|
|
347
|
+
import "forge-std/Test.sol";
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Proof of Concept: Division Before Multiplication Precision Loss
|
|
351
|
+
* Demonstrates how (a/b)*c loses precision vs (a*c)/b
|
|
352
|
+
*/
|
|
353
|
+
contract PrecisionLossExploit is Test {
|
|
354
|
+
function testDivisionBeforeMultiplication() public {
|
|
355
|
+
// Example values that demonstrate precision loss
|
|
356
|
+
uint256 ${dividend} = 1000;
|
|
357
|
+
uint256 ${divisor} = 3;
|
|
358
|
+
uint256 ${multiplier} = 7;
|
|
359
|
+
|
|
360
|
+
// Vulnerable calculation (division first - loses precision)
|
|
361
|
+
uint256 vulnerable = (${dividend} / ${divisor}) * ${multiplier};
|
|
362
|
+
// 1000 / 3 = 333 (truncated)
|
|
363
|
+
// 333 * 7 = 2331
|
|
364
|
+
|
|
365
|
+
// Correct calculation (multiplication first)
|
|
366
|
+
uint256 correct = (${dividend} * ${multiplier}) / ${divisor};
|
|
367
|
+
// 1000 * 7 = 7000
|
|
368
|
+
// 7000 / 3 = 2333
|
|
369
|
+
|
|
370
|
+
console.log("Vulnerable result:", vulnerable);
|
|
371
|
+
console.log("Correct result:", correct);
|
|
372
|
+
console.log("Precision lost:", correct - vulnerable);
|
|
373
|
+
|
|
374
|
+
// The difference of 2 units may seem small, but:
|
|
375
|
+
// - At scale (millions of tokens), this becomes significant
|
|
376
|
+
// - Repeated operations compound the error
|
|
377
|
+
// - Attackers can exploit small amounts to always round in their favor
|
|
378
|
+
|
|
379
|
+
assertGt(correct, vulnerable, "Precision loss demonstrated");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function testExploitSmallAmounts() public {
|
|
383
|
+
// Exploit: Process many small amounts that round to zero
|
|
384
|
+
uint256 amount = 99;
|
|
385
|
+
uint256 feePercent = 1;
|
|
386
|
+
uint256 base = 100;
|
|
387
|
+
|
|
388
|
+
// Vulnerable: fee rounds to zero
|
|
389
|
+
uint256 fee = (amount / base) * feePercent; // 0
|
|
390
|
+
assertEq(fee, 0, "Fee is zero - attacker pays no fees");
|
|
391
|
+
|
|
392
|
+
// Correct: fee is calculated properly
|
|
393
|
+
uint256 correctFee = (amount * feePercent) / base; // 0 still, but...
|
|
394
|
+
|
|
395
|
+
// With slightly larger amount
|
|
396
|
+
amount = 100;
|
|
397
|
+
fee = (amount / base) * feePercent; // 1
|
|
398
|
+
correctFee = (amount * feePercent) / base; // 1
|
|
399
|
+
|
|
400
|
+
// Attacker processes 100 transactions of 99 each: 0 total fees
|
|
401
|
+
// vs 1 transaction of 9900: 99 fees
|
|
402
|
+
console.log("Exploit: Split to avoid fees");
|
|
403
|
+
}
|
|
404
|
+
}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = PrecisionLossDetector;
|