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,409 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Signature Replay Attack Detector
|
|
5
|
+
* Detects vulnerabilities in contracts using off-chain signatures for meta-transactions
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Missing nonce in signature verification
|
|
9
|
+
* - Reusable signatures without expiration
|
|
10
|
+
* - Missing chain ID in signature (cross-chain replay)
|
|
11
|
+
* - Weak signature verification (no EIP-712)
|
|
12
|
+
* - Missing signature validation checks
|
|
13
|
+
* - Replay protection bypasses
|
|
14
|
+
*/
|
|
15
|
+
class SignatureReplayDetector extends BaseDetector {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(
|
|
18
|
+
'Signature Replay Vulnerability',
|
|
19
|
+
'Detects missing replay protection in contracts using off-chain signatures (meta-transactions, permit, etc.)',
|
|
20
|
+
'HIGH'
|
|
21
|
+
);
|
|
22
|
+
this.currentContract = null;
|
|
23
|
+
this.signatureFunctions = [];
|
|
24
|
+
this.nonceTracking = new Map(); // function -> has nonce
|
|
25
|
+
this.eip712Usage = false;
|
|
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
|
+
// Check for EIP-712 usage
|
|
38
|
+
this.eip712Usage = this.detectEIP712Usage(sourceCode);
|
|
39
|
+
|
|
40
|
+
this.traverse(ast);
|
|
41
|
+
|
|
42
|
+
// Post-traversal analysis
|
|
43
|
+
this.analyzeSignatureFunctions();
|
|
44
|
+
|
|
45
|
+
return this.findings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
visitContractDefinition(node) {
|
|
49
|
+
this.currentContract = node.name;
|
|
50
|
+
this.signatureFunctions = [];
|
|
51
|
+
this.nonceTracking = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
visitFunctionDefinition(node) {
|
|
55
|
+
const funcName = node.name || '';
|
|
56
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
57
|
+
const funcCodeLower = funcCode.toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Detect signature verification functions
|
|
60
|
+
if (this.isSignatureFunction(funcCode, funcName)) {
|
|
61
|
+
const sigInfo = {
|
|
62
|
+
name: funcName,
|
|
63
|
+
node: node,
|
|
64
|
+
code: funcCode,
|
|
65
|
+
hasNonce: this.hasNonceProtection(funcCode),
|
|
66
|
+
hasExpiration: this.hasExpirationProtection(funcCode),
|
|
67
|
+
hasChainId: this.hasChainIdProtection(funcCode),
|
|
68
|
+
usesEIP712: this.usesEIP712InFunction(funcCode),
|
|
69
|
+
usesEcrecover: funcCodeLower.includes('ecrecover'),
|
|
70
|
+
usesSignature: funcCodeLower.includes('signature') || funcCodeLower.includes('sig')
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this.signatureFunctions.push(sigInfo);
|
|
74
|
+
this.nonceTracking.set(funcName, sigInfo.hasNonce);
|
|
75
|
+
|
|
76
|
+
// Analyze this function for vulnerabilities
|
|
77
|
+
this.analyzeSignatureFunction(sigInfo);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if function uses signature verification
|
|
83
|
+
*/
|
|
84
|
+
isSignatureFunction(code, funcName) {
|
|
85
|
+
const codeLower = code.toLowerCase();
|
|
86
|
+
const funcNameLower = funcName.toLowerCase();
|
|
87
|
+
|
|
88
|
+
// Common signature function patterns
|
|
89
|
+
const signaturePatterns = [
|
|
90
|
+
/ecrecover\s*\(/i,
|
|
91
|
+
/verify\s*\(.*signature/i,
|
|
92
|
+
/permit\s*\(/i,
|
|
93
|
+
/meta.*transaction/i,
|
|
94
|
+
/off.*chain.*sign/i,
|
|
95
|
+
/ECDSA\.recover/i,
|
|
96
|
+
/SignatureChecker/i,
|
|
97
|
+
/_verify/i,
|
|
98
|
+
/verifySignature/i,
|
|
99
|
+
/checkSignature/i
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// Function name patterns
|
|
103
|
+
const namePatterns = [
|
|
104
|
+
'permit',
|
|
105
|
+
'meta',
|
|
106
|
+
'verify',
|
|
107
|
+
'signature',
|
|
108
|
+
'nonce'
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// IMPORTANT: do NOT classify generic "execute*" functions as signature-based.
|
|
112
|
+
// This prevents false positives on functions like executeFlashLoan(), execute(), etc.
|
|
113
|
+
const hasSignatureOps = signaturePatterns.some(pattern => pattern.test(code));
|
|
114
|
+
const nameIndicatesSig = namePatterns.some(pattern => funcNameLower.includes(pattern));
|
|
115
|
+
|
|
116
|
+
return hasSignatureOps || nameIndicatesSig;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Analyze a signature function for vulnerabilities
|
|
121
|
+
*/
|
|
122
|
+
analyzeSignatureFunction(sigInfo) {
|
|
123
|
+
const { name, code, node, hasNonce, hasExpiration, hasChainId, usesEIP712, usesEcrecover } = sigInfo;
|
|
124
|
+
|
|
125
|
+
// Critical: Missing nonce protection
|
|
126
|
+
if (!hasNonce) {
|
|
127
|
+
this.addFinding({
|
|
128
|
+
title: 'Missing Nonce Protection in Signature Verification',
|
|
129
|
+
description: `Function '${name}' uses signature verification but lacks nonce tracking. Signatures can be replayed multiple times, allowing attackers to execute the same transaction repeatedly.`,
|
|
130
|
+
location: `Contract: ${this.currentContract}, Function: ${name}`,
|
|
131
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
132
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
133
|
+
code: this.getCodeSnippet(node.loc),
|
|
134
|
+
severity: 'CRITICAL',
|
|
135
|
+
confidence: 'HIGH',
|
|
136
|
+
exploitable: true,
|
|
137
|
+
exploitabilityScore: 90,
|
|
138
|
+
attackVector: 'signature-replay',
|
|
139
|
+
recommendation: 'Implement nonce tracking: mapping(address => uint256) public nonces; Increment nonce after each signature use. Include nonce in signature hash.',
|
|
140
|
+
references: [
|
|
141
|
+
'https://swcregistry.io/docs/SWC-121',
|
|
142
|
+
'https://eips.ethereum.org/EIPS/eip-2612',
|
|
143
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/utils#SignatureChecker'
|
|
144
|
+
],
|
|
145
|
+
foundryPoC: this.generateNonceReplayPoC(this.currentContract, name)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// High: Missing expiration
|
|
150
|
+
if (!hasExpiration) {
|
|
151
|
+
this.addFinding({
|
|
152
|
+
title: 'Missing Expiration in Signature',
|
|
153
|
+
description: `Function '${name}' accepts signatures without expiration timestamp. Old signatures remain valid indefinitely, increasing attack surface.`,
|
|
154
|
+
location: `Contract: ${this.currentContract}, Function: ${name}`,
|
|
155
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
156
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
157
|
+
code: this.getCodeSnippet(node.loc),
|
|
158
|
+
severity: 'HIGH',
|
|
159
|
+
confidence: 'HIGH',
|
|
160
|
+
exploitable: true,
|
|
161
|
+
exploitabilityScore: 70,
|
|
162
|
+
attackVector: 'signature-replay',
|
|
163
|
+
recommendation: 'Include deadline/expiration timestamp in signature hash. Verify deadline > block.timestamp before processing.',
|
|
164
|
+
references: [
|
|
165
|
+
'https://eips.ethereum.org/EIPS/eip-2612',
|
|
166
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/utils#SignatureChecker'
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// High: Missing chain ID (cross-chain replay)
|
|
172
|
+
if (!hasChainId && !this.eip712Usage) {
|
|
173
|
+
this.addFinding({
|
|
174
|
+
title: 'Missing Chain ID in Signature (Cross-Chain Replay Risk)',
|
|
175
|
+
description: `Function '${name}' does not include chain ID in signature verification. Signatures valid on one chain can be replayed on another chain (e.g., mainnet signature used on testnet).`,
|
|
176
|
+
location: `Contract: ${this.currentContract}, Function: ${name}`,
|
|
177
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
178
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
179
|
+
code: this.getCodeSnippet(node.loc),
|
|
180
|
+
severity: 'HIGH',
|
|
181
|
+
confidence: 'MEDIUM',
|
|
182
|
+
exploitable: true,
|
|
183
|
+
exploitabilityScore: 75,
|
|
184
|
+
attackVector: 'cross-chain-replay',
|
|
185
|
+
recommendation: 'Include chain ID in signature hash. Use EIP-712 for structured data signing which includes domain separator with chain ID.',
|
|
186
|
+
references: [
|
|
187
|
+
'https://eips.ethereum.org/EIPS/eip-712',
|
|
188
|
+
'https://eips.ethereum.org/EIPS/eip-2612'
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Medium: Using ecrecover directly (not EIP-712)
|
|
194
|
+
if (usesEcrecover && !usesEIP712) {
|
|
195
|
+
this.addFinding({
|
|
196
|
+
title: 'Direct ecrecover Usage Without EIP-712',
|
|
197
|
+
description: `Function '${name}' uses ecrecover directly instead of EIP-712 structured data signing. This is error-prone and lacks type safety. Malformed signatures may be accepted.`,
|
|
198
|
+
location: `Contract: ${this.currentContract}, Function: ${name}`,
|
|
199
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
200
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
201
|
+
code: this.getCodeSnippet(node.loc),
|
|
202
|
+
severity: 'MEDIUM',
|
|
203
|
+
confidence: 'MEDIUM',
|
|
204
|
+
exploitable: false,
|
|
205
|
+
exploitabilityScore: 40,
|
|
206
|
+
attackVector: 'signature-verification',
|
|
207
|
+
recommendation: 'Use EIP-712 for structured data signing. Consider OpenZeppelin\'s SignatureChecker or ECDSA library for safer signature verification.',
|
|
208
|
+
references: [
|
|
209
|
+
'https://eips.ethereum.org/EIPS/eip-712',
|
|
210
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/utils#ECDSA'
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check for weak signature validation
|
|
216
|
+
if (this.hasWeakSignatureValidation(code)) {
|
|
217
|
+
this.addFinding({
|
|
218
|
+
title: 'Weak Signature Validation',
|
|
219
|
+
description: `Function '${name}' has insufficient signature validation. Missing checks for zero address, signature malleability, or invalid signature format.`,
|
|
220
|
+
location: `Contract: ${this.currentContract}, Function: ${name}`,
|
|
221
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
222
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
223
|
+
code: this.getCodeSnippet(node.loc),
|
|
224
|
+
severity: 'MEDIUM',
|
|
225
|
+
confidence: 'MEDIUM',
|
|
226
|
+
exploitable: true,
|
|
227
|
+
exploitabilityScore: 60,
|
|
228
|
+
attackVector: 'signature-validation',
|
|
229
|
+
recommendation: 'Validate recovered address is not zero. Check signature length (65 bytes). Use OpenZeppelin\'s ECDSA.recover which handles malleability.',
|
|
230
|
+
references: [
|
|
231
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/utils#ECDSA',
|
|
232
|
+
'https://swcregistry.io/docs/SWC-117'
|
|
233
|
+
]
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if function has nonce protection
|
|
240
|
+
*/
|
|
241
|
+
hasNonceProtection(code) {
|
|
242
|
+
const codeLower = code.toLowerCase();
|
|
243
|
+
|
|
244
|
+
// Check for nonce usage
|
|
245
|
+
const noncePatterns = [
|
|
246
|
+
/nonces\s*\[/i,
|
|
247
|
+
/nonce\s*\+\+/i,
|
|
248
|
+
/nonce\s*\+=\s*1/i,
|
|
249
|
+
/_useNonce\s*\(/i,
|
|
250
|
+
/incrementNonce/i,
|
|
251
|
+
/nonce.*require/i
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
return noncePatterns.some(pattern => pattern.test(code));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if function has expiration protection
|
|
259
|
+
*/
|
|
260
|
+
hasExpirationProtection(code) {
|
|
261
|
+
const codeLower = code.toLowerCase();
|
|
262
|
+
|
|
263
|
+
const expirationPatterns = [
|
|
264
|
+
/deadline\s*>/i,
|
|
265
|
+
/expires?\s*>/i,
|
|
266
|
+
/expiration\s*>/i,
|
|
267
|
+
/require\s*\(\s*.*deadline/i,
|
|
268
|
+
/require\s*\(\s*.*expir/i,
|
|
269
|
+
/block\.timestamp\s*</i
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
return expirationPatterns.some(pattern => pattern.test(code));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if function includes chain ID
|
|
277
|
+
*/
|
|
278
|
+
hasChainIdProtection(code) {
|
|
279
|
+
const codeLower = code.toLowerCase();
|
|
280
|
+
|
|
281
|
+
const chainIdPatterns = [
|
|
282
|
+
/chainid/i,
|
|
283
|
+
/chain\.id/i,
|
|
284
|
+
/getChainId/i,
|
|
285
|
+
/DOMAIN_SEPARATOR/i,
|
|
286
|
+
/eip712domain/i,
|
|
287
|
+
/typeHash.*chainId/i
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
return chainIdPatterns.some(pattern => pattern.test(code));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if function uses EIP-712
|
|
295
|
+
*/
|
|
296
|
+
usesEIP712InFunction(code) {
|
|
297
|
+
const codeLower = code.toLowerCase();
|
|
298
|
+
|
|
299
|
+
return /eip712|DOMAIN_SEPARATOR|_TYPE_HASH|_HASHED_NAME|_HASHED_VERSION/i.test(codeLower);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if contract uses EIP-712 overall
|
|
304
|
+
*/
|
|
305
|
+
detectEIP712Usage(sourceCode) {
|
|
306
|
+
const codeLower = sourceCode.toLowerCase();
|
|
307
|
+
return /eip712|DOMAIN_SEPARATOR|_TYPE_HASH/i.test(codeLower);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check for weak signature validation
|
|
312
|
+
*/
|
|
313
|
+
hasWeakSignatureValidation(code) {
|
|
314
|
+
const codeLower = code.toLowerCase();
|
|
315
|
+
|
|
316
|
+
// Should check for zero address
|
|
317
|
+
const hasZeroCheck = /require\s*\(\s*.*!=\s*address\(0\)|require\s*\(\s*.*!=\s*0x0/i.test(codeLower);
|
|
318
|
+
|
|
319
|
+
// Should check signature length
|
|
320
|
+
const hasLengthCheck = /length\s*==\s*65|length\s*==\s*64/i.test(codeLower);
|
|
321
|
+
|
|
322
|
+
// Using ecrecover without proper validation
|
|
323
|
+
const usesEcrecover = /ecrecover\s*\(/i.test(codeLower);
|
|
324
|
+
|
|
325
|
+
return usesEcrecover && (!hasZeroCheck || !hasLengthCheck);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Post-traversal analysis
|
|
330
|
+
*/
|
|
331
|
+
analyzeSignatureFunctions() {
|
|
332
|
+
// Check if contract has nonce mapping but functions don't use it
|
|
333
|
+
const hasNonceMapping = /mapping\s*\(.*\)\s*public\s*nonces/i.test(this.sourceCode);
|
|
334
|
+
|
|
335
|
+
if (hasNonceMapping) {
|
|
336
|
+
// Check if any signature function doesn't use it
|
|
337
|
+
this.signatureFunctions.forEach(sigInfo => {
|
|
338
|
+
if (!sigInfo.hasNonce) {
|
|
339
|
+
this.addFinding({
|
|
340
|
+
title: 'Nonce Mapping Exists But Not Used',
|
|
341
|
+
description: `Contract has nonce mapping but function '${sigInfo.name}' does not use it. This suggests incomplete implementation of replay protection.`,
|
|
342
|
+
location: `Contract: ${this.currentContract}, Function: ${sigInfo.name}`,
|
|
343
|
+
line: sigInfo.node.loc ? sigInfo.node.loc.start.line : 0,
|
|
344
|
+
column: sigInfo.node.loc ? sigInfo.node.loc.start.column : 0,
|
|
345
|
+
code: this.getCodeSnippet(sigInfo.node.loc),
|
|
346
|
+
severity: 'HIGH',
|
|
347
|
+
confidence: 'HIGH',
|
|
348
|
+
exploitable: true,
|
|
349
|
+
exploitabilityScore: 80,
|
|
350
|
+
attackVector: 'signature-replay',
|
|
351
|
+
recommendation: 'Ensure all signature verification functions increment and check nonces. Use _useNonce(address) pattern from OpenZeppelin.',
|
|
352
|
+
references: [
|
|
353
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/utils#SignatureChecker'
|
|
354
|
+
]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Generate Foundry PoC for nonce replay attack
|
|
363
|
+
*/
|
|
364
|
+
generateNonceReplayPoC(contractName, funcName) {
|
|
365
|
+
return `// SPDX-License-Identifier: MIT
|
|
366
|
+
pragma solidity ^0.8.0;
|
|
367
|
+
|
|
368
|
+
import "forge-std/Test.sol";
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Proof of Concept: Signature Replay Attack
|
|
372
|
+
* Target: ${contractName}.${funcName}()
|
|
373
|
+
* Attack Vector: Reuse same signature multiple times
|
|
374
|
+
*/
|
|
375
|
+
contract SignatureReplayExploit is Test {
|
|
376
|
+
address constant TARGET = address(0); // ${contractName} address
|
|
377
|
+
address attacker = address(this);
|
|
378
|
+
|
|
379
|
+
uint256 privateKey = 0x1234...; // Attacker's private key
|
|
380
|
+
address signer = vm.addr(privateKey);
|
|
381
|
+
|
|
382
|
+
function testExploit() public {
|
|
383
|
+
// 1. Create signature for legitimate transaction
|
|
384
|
+
// bytes memory sig = signTransaction(...);
|
|
385
|
+
|
|
386
|
+
// 2. Execute transaction with signature (first time - legitimate)
|
|
387
|
+
// ${contractName}(TARGET).${funcName}(..., sig);
|
|
388
|
+
|
|
389
|
+
// 3. Replay the same signature (should fail but doesn't)
|
|
390
|
+
// ${contractName}(TARGET).${funcName}(..., sig); // Replay!
|
|
391
|
+
|
|
392
|
+
// 4. If nonce not checked, this succeeds and attacker benefits twice
|
|
393
|
+
|
|
394
|
+
// Assert replay succeeded
|
|
395
|
+
// assertGt(attackerBalance, expectedBalance);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Helper: Sign transaction
|
|
399
|
+
// function signTransaction(...) internal returns (bytes memory) {
|
|
400
|
+
// bytes32 hash = keccak256(abi.encodePacked(...));
|
|
401
|
+
// (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash);
|
|
402
|
+
// return abi.encodePacked(r, s, v);
|
|
403
|
+
// }
|
|
404
|
+
}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = SignatureReplayDetector;
|
|
409
|
+
|