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,368 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Permit and Approval Exploits Detector
|
|
5
|
+
* Detects vulnerabilities in token approval mechanisms:
|
|
6
|
+
* - ERC20 Permit front-running
|
|
7
|
+
* - Approval race conditions
|
|
8
|
+
* - Unlimited approval risks
|
|
9
|
+
* - Permit2 integration issues
|
|
10
|
+
* - Phantom function exploits
|
|
11
|
+
*
|
|
12
|
+
* Immunefi Critical: Direct theft of approved tokens
|
|
13
|
+
*/
|
|
14
|
+
class PermitExploitsDetector extends BaseDetector {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(
|
|
17
|
+
'Permit/Approval Exploits',
|
|
18
|
+
'Detects vulnerabilities in token approval mechanisms',
|
|
19
|
+
'CRITICAL'
|
|
20
|
+
);
|
|
21
|
+
this.currentContract = null;
|
|
22
|
+
this.currentFunction = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
26
|
+
this.findings = [];
|
|
27
|
+
this.ast = ast;
|
|
28
|
+
this.sourceCode = sourceCode;
|
|
29
|
+
this.fileName = fileName;
|
|
30
|
+
this.sourceLines = sourceCode.split('\n');
|
|
31
|
+
this.cfg = cfg;
|
|
32
|
+
this.dataFlow = dataFlow;
|
|
33
|
+
|
|
34
|
+
this.traverse(ast);
|
|
35
|
+
|
|
36
|
+
return this.findings;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
visitContractDefinition(node) {
|
|
40
|
+
this.currentContract = node.name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
visitFunctionDefinition(node) {
|
|
44
|
+
this.currentFunction = node.name || 'constructor';
|
|
45
|
+
|
|
46
|
+
if (!node.body) return;
|
|
47
|
+
|
|
48
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
49
|
+
const funcName = (node.name || '').toLowerCase();
|
|
50
|
+
|
|
51
|
+
// Skip internal functions for direct exploitability
|
|
52
|
+
if (node.visibility === 'private' || node.visibility === 'internal') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Detect permit usage patterns
|
|
57
|
+
this.detectPermitVulnerabilities(funcCode, node, funcName);
|
|
58
|
+
|
|
59
|
+
// Detect approval patterns
|
|
60
|
+
this.detectApprovalVulnerabilities(funcCode, node, funcName);
|
|
61
|
+
|
|
62
|
+
// Detect transferFrom patterns
|
|
63
|
+
this.detectTransferFromVulnerabilities(funcCode, node, funcName);
|
|
64
|
+
|
|
65
|
+
// Detect Permit2 patterns
|
|
66
|
+
this.detectPermit2Vulnerabilities(funcCode, node, funcName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect ERC20 Permit vulnerabilities
|
|
71
|
+
*/
|
|
72
|
+
detectPermitVulnerabilities(funcCode, node, funcName) {
|
|
73
|
+
// Check for permit calls
|
|
74
|
+
const hasPermit = /\.permit\s*\(/.test(funcCode);
|
|
75
|
+
|
|
76
|
+
if (hasPermit) {
|
|
77
|
+
// Check for permit griefing (DoS via front-running)
|
|
78
|
+
if (/try\s*.*\.permit|\.permit.*catch/.test(funcCode)) {
|
|
79
|
+
// Has try-catch, likely protected
|
|
80
|
+
} else {
|
|
81
|
+
this.addFinding({
|
|
82
|
+
title: 'Permit Front-Running DoS',
|
|
83
|
+
description: `Function '${this.currentFunction}' calls permit() without try-catch protection. An attacker can front-run the transaction by using the same permit, causing the original transaction to revert.
|
|
84
|
+
|
|
85
|
+
Attack scenario:
|
|
86
|
+
1. User signs permit and submits tx calling ${this.currentFunction}
|
|
87
|
+
2. Attacker sees tx in mempool, extracts permit signature
|
|
88
|
+
3. Attacker front-runs with own tx using the same permit
|
|
89
|
+
4. User's tx reverts because permit is already used (nonce consumed)
|
|
90
|
+
|
|
91
|
+
This is a DoS attack that wastes user gas and blocks functionality.`,
|
|
92
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
93
|
+
line: node.loc?.start?.line || 0,
|
|
94
|
+
column: node.loc?.start?.column || 0,
|
|
95
|
+
code: funcCode.substring(0, 300),
|
|
96
|
+
severity: 'HIGH',
|
|
97
|
+
confidence: 'HIGH',
|
|
98
|
+
exploitable: true,
|
|
99
|
+
exploitabilityScore: 75,
|
|
100
|
+
attackVector: 'permit-dos',
|
|
101
|
+
recommendation: `Wrap permit calls in try-catch:
|
|
102
|
+
try token.permit(owner, spender, value, deadline, v, r, s) {} catch {}
|
|
103
|
+
|
|
104
|
+
The function should continue even if permit fails (allowance may already exist).
|
|
105
|
+
Alternative: Check allowance before calling permit.`,
|
|
106
|
+
references: [
|
|
107
|
+
'https://www.trust-security.xyz/post/permission-denied',
|
|
108
|
+
'https://eips.ethereum.org/EIPS/eip-2612'
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check for permit replay (missing nonce/deadline validation)
|
|
114
|
+
if (/permit.*deadline/i.test(funcCode)) {
|
|
115
|
+
if (!/deadline\s*>=\s*block\.timestamp|block\.timestamp\s*<=\s*deadline|require.*deadline/i.test(funcCode)) {
|
|
116
|
+
this.addFinding({
|
|
117
|
+
title: 'Permit Deadline Not Validated',
|
|
118
|
+
description: `Function '${this.currentFunction}' uses permit but may not properly validate the deadline. Expired permits should be rejected.`,
|
|
119
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
120
|
+
line: node.loc?.start?.line || 0,
|
|
121
|
+
code: funcCode.substring(0, 200),
|
|
122
|
+
severity: 'MEDIUM',
|
|
123
|
+
confidence: 'MEDIUM',
|
|
124
|
+
exploitable: true,
|
|
125
|
+
exploitabilityScore: 60,
|
|
126
|
+
attackVector: 'expired-permit',
|
|
127
|
+
recommendation: 'Ensure permit implementation validates: require(deadline >= block.timestamp, "Permit expired")'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check for permit implementation vulnerabilities
|
|
134
|
+
if (/function\s+permit\s*\(/i.test(funcCode)) {
|
|
135
|
+
// DOMAIN_SEPARATOR issues
|
|
136
|
+
if (/DOMAIN_SEPARATOR|_domainSeparator/i.test(funcCode)) {
|
|
137
|
+
if (!/block\.chainid|chainId/i.test(funcCode)) {
|
|
138
|
+
this.addFinding({
|
|
139
|
+
title: 'Permit Missing Chain ID Validation',
|
|
140
|
+
description: `Permit implementation in '${this.currentFunction}' may not validate chain ID. Permits signed on one chain could be replayed on forks or other chains.`,
|
|
141
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
142
|
+
line: node.loc?.start?.line || 0,
|
|
143
|
+
code: funcCode.substring(0, 200),
|
|
144
|
+
severity: 'HIGH',
|
|
145
|
+
confidence: 'MEDIUM',
|
|
146
|
+
exploitable: true,
|
|
147
|
+
exploitabilityScore: 75,
|
|
148
|
+
attackVector: 'cross-chain-permit-replay',
|
|
149
|
+
recommendation: 'Include chainId in DOMAIN_SEPARATOR and recompute on chain ID change: if (block.chainid != INITIAL_CHAIN_ID) return _computeDomainSeparator();'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Nonce handling
|
|
155
|
+
if (!/nonces\s*\[|_useNonce|nonce\s*\+\+/i.test(funcCode)) {
|
|
156
|
+
this.addFinding({
|
|
157
|
+
title: 'Permit Missing Nonce Protection',
|
|
158
|
+
description: `Permit implementation in '${this.currentFunction}' may not properly handle nonces. Without nonce tracking, permits can be replayed.`,
|
|
159
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
160
|
+
line: node.loc?.start?.line || 0,
|
|
161
|
+
code: funcCode.substring(0, 200),
|
|
162
|
+
severity: 'CRITICAL',
|
|
163
|
+
confidence: 'MEDIUM',
|
|
164
|
+
exploitable: true,
|
|
165
|
+
exploitabilityScore: 90,
|
|
166
|
+
attackVector: 'permit-replay',
|
|
167
|
+
recommendation: 'Implement nonce tracking: require(nonce == nonces[owner]++, "Invalid nonce")'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Detect approval vulnerabilities
|
|
175
|
+
*/
|
|
176
|
+
detectApprovalVulnerabilities(funcCode, node, funcName) {
|
|
177
|
+
// Approval race condition (ERC20 approve)
|
|
178
|
+
if (/function\s+approve\s*\(/.test(funcCode)) {
|
|
179
|
+
// Check for standard approve without increaseAllowance pattern
|
|
180
|
+
if (!/require.*allowance.*==\s*0|safeApprove|increaseAllowance/i.test(funcCode)) {
|
|
181
|
+
this.addFinding({
|
|
182
|
+
title: 'ERC20 Approval Race Condition',
|
|
183
|
+
description: `Standard approve() in '${this.currentFunction}' is vulnerable to race condition:
|
|
184
|
+
|
|
185
|
+
1. User has approved spender for 100 tokens
|
|
186
|
+
2. User wants to change approval to 50 tokens
|
|
187
|
+
3. User sends approve(spender, 50) transaction
|
|
188
|
+
4. Spender sees pending tx, front-runs to transferFrom(100)
|
|
189
|
+
5. User's approve(50) executes
|
|
190
|
+
6. Spender now has additional 50 allowance (150 total extracted)
|
|
191
|
+
|
|
192
|
+
This is a known ERC20 issue affecting token safety.`,
|
|
193
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
194
|
+
line: node.loc?.start?.line || 0,
|
|
195
|
+
code: funcCode.substring(0, 200),
|
|
196
|
+
severity: 'MEDIUM',
|
|
197
|
+
confidence: 'HIGH',
|
|
198
|
+
exploitable: true,
|
|
199
|
+
exploitabilityScore: 60,
|
|
200
|
+
attackVector: 'approval-race',
|
|
201
|
+
recommendation: `Implement increaseAllowance/decreaseAllowance pattern:
|
|
202
|
+
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
|
|
203
|
+
_approve(msg.sender, spender, allowance[msg.sender][spender] + addedValue);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
Or require current allowance is 0: require(allowance == 0 || newAllowance == 0);`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Unlimited approval griefing
|
|
213
|
+
if (/approve.*type\s*\(\s*uint256\s*\)\.max|approve.*MAX_UINT|0xffffffff/i.test(funcCode)) {
|
|
214
|
+
this.addFinding({
|
|
215
|
+
title: 'Unlimited Token Approval',
|
|
216
|
+
description: `Function '${this.currentFunction}' sets unlimited approval (type(uint256).max). If the approved contract is compromised, all user tokens can be stolen.`,
|
|
217
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
218
|
+
line: node.loc?.start?.line || 0,
|
|
219
|
+
code: funcCode.substring(0, 200),
|
|
220
|
+
severity: 'MEDIUM',
|
|
221
|
+
confidence: 'HIGH',
|
|
222
|
+
exploitable: true,
|
|
223
|
+
exploitabilityScore: 55,
|
|
224
|
+
attackVector: 'unlimited-approval',
|
|
225
|
+
recommendation: 'Consider using exact amounts for approvals or implementing a approval management pattern. Document risks to users.'
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Detect transferFrom vulnerabilities
|
|
232
|
+
*/
|
|
233
|
+
detectTransferFromVulnerabilities(funcCode, node, funcName) {
|
|
234
|
+
// Phantom function vulnerability (non-standard token handling)
|
|
235
|
+
if (/\.transferFrom\s*\(/.test(funcCode)) {
|
|
236
|
+
// Check if return value is checked
|
|
237
|
+
if (!/require.*transferFrom|bool.*=.*transferFrom|safeTransferFrom/i.test(funcCode)) {
|
|
238
|
+
this.addFinding({
|
|
239
|
+
title: 'Unchecked transferFrom Return Value',
|
|
240
|
+
description: `Function '${this.currentFunction}' calls transferFrom without checking return value. Some tokens (USDT, BNB) don't revert on failure but return false.
|
|
241
|
+
|
|
242
|
+
Attack scenario:
|
|
243
|
+
1. User calls function with failing token transfer
|
|
244
|
+
2. transferFrom returns false but execution continues
|
|
245
|
+
3. Contract state updated as if transfer succeeded
|
|
246
|
+
4. Attacker extracts value without actually paying`,
|
|
247
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
248
|
+
line: node.loc?.start?.line || 0,
|
|
249
|
+
code: funcCode.substring(0, 200),
|
|
250
|
+
severity: 'HIGH',
|
|
251
|
+
confidence: 'HIGH',
|
|
252
|
+
exploitable: true,
|
|
253
|
+
exploitabilityScore: 80,
|
|
254
|
+
attackVector: 'unchecked-transfer',
|
|
255
|
+
recommendation: `Use SafeERC20.safeTransferFrom() from OpenZeppelin:
|
|
256
|
+
using SafeERC20 for IERC20;
|
|
257
|
+
token.safeTransferFrom(from, to, amount);
|
|
258
|
+
|
|
259
|
+
Or explicitly check: require(token.transferFrom(from, to, amount), "Transfer failed");`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Fee-on-transfer token vulnerability
|
|
265
|
+
if (/transferFrom.*amount|amount.*transferFrom/i.test(funcCode)) {
|
|
266
|
+
// Check if actual received amount is validated
|
|
267
|
+
if (!/balanceOf.*after|balance.*before.*after|actualAmount|received/i.test(funcCode)) {
|
|
268
|
+
this.addFinding({
|
|
269
|
+
title: 'Fee-on-Transfer Token Not Handled',
|
|
270
|
+
description: `Function '${this.currentFunction}' assumes transferFrom amount equals received amount. Fee-on-transfer tokens (like USDT with fees enabled) deduct fees, causing accounting errors.
|
|
271
|
+
|
|
272
|
+
This can lead to:
|
|
273
|
+
- Protocol insolvency (crediting more than received)
|
|
274
|
+
- Failed withdrawals (less tokens than expected)
|
|
275
|
+
- Share price manipulation`,
|
|
276
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
277
|
+
line: node.loc?.start?.line || 0,
|
|
278
|
+
code: funcCode.substring(0, 200),
|
|
279
|
+
severity: 'HIGH',
|
|
280
|
+
confidence: 'MEDIUM',
|
|
281
|
+
exploitable: true,
|
|
282
|
+
exploitabilityScore: 70,
|
|
283
|
+
attackVector: 'fee-on-transfer',
|
|
284
|
+
recommendation: `Measure actual received amount:
|
|
285
|
+
uint256 balanceBefore = token.balanceOf(address(this));
|
|
286
|
+
token.safeTransferFrom(from, address(this), amount);
|
|
287
|
+
uint256 actualReceived = token.balanceOf(address(this)) - balanceBefore;
|
|
288
|
+
// Use actualReceived for accounting`
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Detect Permit2 specific vulnerabilities
|
|
296
|
+
*/
|
|
297
|
+
detectPermit2Vulnerabilities(funcCode, node, funcName) {
|
|
298
|
+
const usesPermit2 = /permit2|IPermit2|ISignatureTransfer|IAllowanceTransfer/i.test(funcCode);
|
|
299
|
+
|
|
300
|
+
if (!usesPermit2) return;
|
|
301
|
+
|
|
302
|
+
// Permit2 witness data validation
|
|
303
|
+
if (/permitWitness|signatureTransfer/i.test(funcCode)) {
|
|
304
|
+
if (!/verify.*witness|witness.*hash|keccak.*witness/i.test(funcCode)) {
|
|
305
|
+
this.addFinding({
|
|
306
|
+
title: 'Permit2 Witness Data Not Validated',
|
|
307
|
+
description: `Function '${this.currentFunction}' uses Permit2 witness but may not properly validate witness data. Attacker could substitute malicious parameters.`,
|
|
308
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
309
|
+
line: node.loc?.start?.line || 0,
|
|
310
|
+
code: funcCode.substring(0, 200),
|
|
311
|
+
severity: 'HIGH',
|
|
312
|
+
confidence: 'MEDIUM',
|
|
313
|
+
exploitable: true,
|
|
314
|
+
exploitabilityScore: 75,
|
|
315
|
+
attackVector: 'permit2-witness-bypass',
|
|
316
|
+
recommendation: 'Ensure witness data is properly hashed and verified in the signature. Include all critical parameters in witness.'
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Permit2 allowance vs signature transfer confusion
|
|
322
|
+
if (/allowanceTransfer|transferFrom.*permit2/i.test(funcCode)) {
|
|
323
|
+
this.addFinding({
|
|
324
|
+
title: 'Permit2 Integration Detected',
|
|
325
|
+
description: `Function '${this.currentFunction}' uses Permit2 allowance transfers. Ensure proper integration:
|
|
326
|
+
- Validate permit2 address is correct
|
|
327
|
+
- Handle both single and batch transfers
|
|
328
|
+
- Verify nonce handling for replay protection`,
|
|
329
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
330
|
+
line: node.loc?.start?.line || 0,
|
|
331
|
+
code: funcCode.substring(0, 150),
|
|
332
|
+
severity: 'MEDIUM',
|
|
333
|
+
confidence: 'LOW',
|
|
334
|
+
exploitable: false,
|
|
335
|
+
exploitabilityScore: 45,
|
|
336
|
+
attackVector: 'permit2-integration',
|
|
337
|
+
recommendation: 'Audit Permit2 integration carefully. Use official Permit2 library and validate all parameters.'
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Visit member access for specific patterns
|
|
344
|
+
*/
|
|
345
|
+
visitMemberAccess(node) {
|
|
346
|
+
const memberName = node.memberName;
|
|
347
|
+
|
|
348
|
+
// Detect safeApprove with non-zero value
|
|
349
|
+
if (memberName === 'safeApprove') {
|
|
350
|
+
// safeApprove is deprecated and reverts if current allowance != 0
|
|
351
|
+
// This can cause DoS if allowance wasn't fully used
|
|
352
|
+
this.addFinding({
|
|
353
|
+
title: 'Deprecated safeApprove Usage',
|
|
354
|
+
description: `Contract uses deprecated safeApprove which reverts if current allowance is non-zero. This can cause DoS if previous allowance wasn't fully consumed.`,
|
|
355
|
+
location: `Contract: ${this.currentContract}`,
|
|
356
|
+
line: node.loc?.start?.line || 0,
|
|
357
|
+
severity: 'MEDIUM',
|
|
358
|
+
confidence: 'HIGH',
|
|
359
|
+
exploitable: true,
|
|
360
|
+
exploitabilityScore: 55,
|
|
361
|
+
attackVector: 'safeapprove-dos',
|
|
362
|
+
recommendation: 'Use forceApprove() or approve(0) before approve(amount): token.approve(spender, 0); token.approve(spender, amount);'
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = PermitExploitsDetector;
|