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,416 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rebasing Token Vault Detector
|
|
5
|
+
*
|
|
6
|
+
* Detects critical vulnerabilities when vaults/pools integrate with rebasing tokens:
|
|
7
|
+
* - Aave aTokens (balance increases from interest)
|
|
8
|
+
* - Lido stETH (balance changes from staking rewards/slashing)
|
|
9
|
+
* - Ampleforth AMPL (global rebases)
|
|
10
|
+
* - Compound cTokens (exchange rate appreciation)
|
|
11
|
+
*
|
|
12
|
+
* Attack vectors:
|
|
13
|
+
* 1. Share dilution: Rebasing rewards go to vault, not depositors
|
|
14
|
+
* 2. First depositor + rebase: Attacker deposits 1 wei, waits for rebase, steals from next depositor
|
|
15
|
+
* 3. Accounting mismatch: Vault tracks nominal amounts, actual balance differs
|
|
16
|
+
* 4. Negative rebase: Slashing causes insufficient funds for withdrawals
|
|
17
|
+
*
|
|
18
|
+
* Immunefi Critical: Direct theft via accounting manipulation
|
|
19
|
+
*/
|
|
20
|
+
class RebasingTokenVaultDetector extends BaseDetector {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(
|
|
23
|
+
'Rebasing Token Vault',
|
|
24
|
+
'Detects vulnerabilities in vaults integrating rebasing tokens (aTokens, stETH, AMPL)',
|
|
25
|
+
'CRITICAL'
|
|
26
|
+
);
|
|
27
|
+
this.currentContract = null;
|
|
28
|
+
this.currentFunction = null;
|
|
29
|
+
this.rebasingPatterns = [];
|
|
30
|
+
this.vaultPatterns = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
34
|
+
this.findings = [];
|
|
35
|
+
this.ast = ast;
|
|
36
|
+
this.sourceCode = sourceCode;
|
|
37
|
+
this.fileName = fileName;
|
|
38
|
+
this.sourceLines = sourceCode.split('\n');
|
|
39
|
+
this.cfg = cfg;
|
|
40
|
+
this.dataFlow = dataFlow;
|
|
41
|
+
this.rebasingPatterns = [];
|
|
42
|
+
this.vaultPatterns = [];
|
|
43
|
+
|
|
44
|
+
// First pass: identify rebasing token usage and vault patterns
|
|
45
|
+
this.detectRebasingTokenUsage();
|
|
46
|
+
this.detectVaultPatterns();
|
|
47
|
+
|
|
48
|
+
// Traverse AST for detailed analysis
|
|
49
|
+
this.traverse(ast);
|
|
50
|
+
|
|
51
|
+
// Analyze interactions
|
|
52
|
+
this.analyzeRebasingVaultInteractions();
|
|
53
|
+
|
|
54
|
+
return this.findings;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Detect rebasing token interfaces/usage in code
|
|
59
|
+
*/
|
|
60
|
+
detectRebasingTokenUsage() {
|
|
61
|
+
const code = this.sourceCode;
|
|
62
|
+
|
|
63
|
+
// Aave aToken patterns
|
|
64
|
+
const aavePatterns = [
|
|
65
|
+
{ pattern: /IAToken|AToken|aToken/i, type: 'Aave aToken', risk: 'CRITICAL' },
|
|
66
|
+
{ pattern: /IPool\.supply|lendingPool\.deposit/i, type: 'Aave deposit', risk: 'HIGH' },
|
|
67
|
+
{ pattern: /scaledBalanceOf|getScaledUserBalanceAndSupply/i, type: 'Aave scaled balance', risk: 'MEDIUM' },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// Lido stETH patterns
|
|
71
|
+
const lidoPatterns = [
|
|
72
|
+
{ pattern: /IStETH|stETH|wstETH/i, type: 'Lido stETH', risk: 'CRITICAL' },
|
|
73
|
+
{ pattern: /getSharesByPooledEth|getPooledEthByShares/i, type: 'Lido share conversion', risk: 'HIGH' },
|
|
74
|
+
{ pattern: /submit\s*\(\s*\)/i, type: 'Lido staking', risk: 'MEDIUM' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Ampleforth/elastic supply patterns
|
|
78
|
+
const elasticPatterns = [
|
|
79
|
+
{ pattern: /IAMPL|Ampleforth|rebase/i, type: 'Elastic supply', risk: 'CRITICAL' },
|
|
80
|
+
{ pattern: /scaledTotalSupply|scaledBalance/i, type: 'Scaled accounting', risk: 'HIGH' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Compound cToken patterns
|
|
84
|
+
const compoundPatterns = [
|
|
85
|
+
{ pattern: /ICToken|cToken|CErc20/i, type: 'Compound cToken', risk: 'HIGH' },
|
|
86
|
+
{ pattern: /exchangeRateCurrent|exchangeRateStored/i, type: 'Compound exchange rate', risk: 'MEDIUM' },
|
|
87
|
+
{ pattern: /mint\s*\(.*\)|redeem\s*\(.*\)|redeemUnderlying/i, type: 'Compound ops', risk: 'MEDIUM' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const allPatterns = [...aavePatterns, ...lidoPatterns, ...elasticPatterns, ...compoundPatterns];
|
|
91
|
+
|
|
92
|
+
for (const { pattern, type, risk } of allPatterns) {
|
|
93
|
+
if (pattern.test(code)) {
|
|
94
|
+
this.rebasingPatterns.push({ type, risk, pattern });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Detect vault/share-based contract patterns
|
|
101
|
+
*/
|
|
102
|
+
detectVaultPatterns() {
|
|
103
|
+
const code = this.sourceCode;
|
|
104
|
+
|
|
105
|
+
const vaultIndicators = [
|
|
106
|
+
/totalAssets|totalShares|totalSupply/i,
|
|
107
|
+
/convertToAssets|convertToShares|previewDeposit|previewWithdraw/i,
|
|
108
|
+
/deposit.*shares|withdraw.*assets/i,
|
|
109
|
+
/ERC4626|Vault|Pool|Strategy/i,
|
|
110
|
+
/function\s+deposit\s*\(|function\s+withdraw\s*\(/i,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
this.isVaultContract = vaultIndicators.some(p => p.test(code));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
visitContractDefinition(node) {
|
|
117
|
+
this.currentContract = node.name;
|
|
118
|
+
|
|
119
|
+
// Check inheritance for vault patterns
|
|
120
|
+
const baseContracts = (node.baseContracts || [])
|
|
121
|
+
.map(b => b.baseName?.namePath || '')
|
|
122
|
+
.join(' ');
|
|
123
|
+
|
|
124
|
+
if (/ERC4626|Vault|Strategy|Pool/i.test(baseContracts)) {
|
|
125
|
+
this.isVaultContract = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
visitFunctionDefinition(node) {
|
|
130
|
+
this.currentFunction = node.name || 'constructor';
|
|
131
|
+
|
|
132
|
+
if (!node.body) return;
|
|
133
|
+
|
|
134
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
135
|
+
const funcName = (node.name || '').toLowerCase();
|
|
136
|
+
|
|
137
|
+
// Skip internal/private
|
|
138
|
+
if (node.visibility === 'private' || node.visibility === 'internal') {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Analyze deposit/withdraw for rebasing issues
|
|
143
|
+
if (/deposit|stake|supply/i.test(funcName)) {
|
|
144
|
+
this.analyzeDepositFunction(funcCode, node);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (/withdraw|redeem|unstake/i.test(funcName)) {
|
|
148
|
+
this.analyzeWithdrawFunction(funcCode, node);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for totalAssets using balanceOf (vulnerable to rebasing)
|
|
152
|
+
this.checkTotalAssetsImplementation(funcCode, node);
|
|
153
|
+
|
|
154
|
+
// Check for share calculation vulnerabilities
|
|
155
|
+
this.checkShareCalculation(funcCode, node);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Analyze deposit functions for rebasing vulnerabilities
|
|
160
|
+
*/
|
|
161
|
+
analyzeDepositFunction(funcCode, node) {
|
|
162
|
+
if (this.rebasingPatterns.length === 0) return;
|
|
163
|
+
|
|
164
|
+
// Check if deposit uses raw balanceOf for share calculation
|
|
165
|
+
const usesBalanceOf = /balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/i.test(funcCode);
|
|
166
|
+
const usesTransferAmount = /amount|_amount|assets/i.test(funcCode);
|
|
167
|
+
|
|
168
|
+
// Vulnerable pattern: shares = amount * totalShares / balanceOf(this)
|
|
169
|
+
// If token rebases between deposit transactions, share calculation is wrong
|
|
170
|
+
if (usesBalanceOf && !this.hasRebaseProtection(funcCode)) {
|
|
171
|
+
const rebasingType = this.rebasingPatterns[0].type;
|
|
172
|
+
|
|
173
|
+
this.addFinding({
|
|
174
|
+
title: 'Rebasing Token Deposit Vulnerability',
|
|
175
|
+
description: `Deposit function '${this.currentFunction}' uses balanceOf for share calculation with ${rebasingType}.\n\n` +
|
|
176
|
+
`Attack scenario:\n` +
|
|
177
|
+
`1. Attacker deposits 1 wei, gets 1 share\n` +
|
|
178
|
+
`2. ${rebasingType} rebases (interest accrues)\n` +
|
|
179
|
+
`3. Vault's balanceOf increases WITHOUT new deposits\n` +
|
|
180
|
+
`4. Next depositor's shares = amount * 1 / (1 + rebaseAmount)\n` +
|
|
181
|
+
`5. Depositor gets fewer shares than deserved\n` +
|
|
182
|
+
`6. Attacker withdraws with inflated share value\n\n` +
|
|
183
|
+
`This is a variant of the ERC4626 inflation attack specific to rebasing tokens.`,
|
|
184
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
185
|
+
line: node.loc?.start?.line || 0,
|
|
186
|
+
column: node.loc?.start?.column || 0,
|
|
187
|
+
code: funcCode.substring(0, 400),
|
|
188
|
+
severity: 'CRITICAL',
|
|
189
|
+
confidence: 'HIGH',
|
|
190
|
+
exploitable: true,
|
|
191
|
+
exploitabilityScore: 95,
|
|
192
|
+
attackVector: 'rebasing-token-vault',
|
|
193
|
+
recommendation: `For ${rebasingType}:\n` +
|
|
194
|
+
`1. Use internal accounting (track deposited amounts, not balanceOf)\n` +
|
|
195
|
+
`2. For Aave: Use scaledBalanceOf instead of balanceOf\n` +
|
|
196
|
+
`3. For Lido: Use wstETH (non-rebasing wrapper) or track shares\n` +
|
|
197
|
+
`4. Add virtual shares/assets offset (OZ recommendation)\n` +
|
|
198
|
+
`5. Consider fee-on-transfer style balance checking`,
|
|
199
|
+
references: [
|
|
200
|
+
'https://docs.aave.com/developers/tokens/atoken',
|
|
201
|
+
'https://docs.lido.fi/contracts/wsteth',
|
|
202
|
+
'https://blog.openzeppelin.com/a-]novel-defense-against-erc4626-inflation-attacks'
|
|
203
|
+
],
|
|
204
|
+
foundryPoC: this.generateRebasingDepositPoC(rebasingType)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Analyze withdraw functions for rebasing vulnerabilities
|
|
211
|
+
*/
|
|
212
|
+
analyzeWithdrawFunction(funcCode, node) {
|
|
213
|
+
if (this.rebasingPatterns.length === 0) return;
|
|
214
|
+
|
|
215
|
+
// Check for insufficient balance handling during negative rebases
|
|
216
|
+
const checksBalance = /require\s*\(.*balance|if\s*\(.*balance\s*</i.test(funcCode);
|
|
217
|
+
const hasSlashingProtection = /slashing|negativeRebase|minBalance/i.test(funcCode);
|
|
218
|
+
|
|
219
|
+
// Vulnerable to Lido slashing events
|
|
220
|
+
if (this.rebasingPatterns.some(p => p.type.includes('Lido')) && !hasSlashingProtection) {
|
|
221
|
+
this.addFinding({
|
|
222
|
+
title: 'Negative Rebase (Slashing) Not Handled',
|
|
223
|
+
description: `Withdraw function '${this.currentFunction}' integrates Lido stETH but doesn't handle slashing scenarios.\n\n` +
|
|
224
|
+
`Risk:\n` +
|
|
225
|
+
`1. Validator slashing event occurs\n` +
|
|
226
|
+
`2. stETH balance decreases (negative rebase)\n` +
|
|
227
|
+
`3. Vault has insufficient tokens for all withdrawal claims\n` +
|
|
228
|
+
`4. Last withdrawers cannot withdraw (bank run)\n` +
|
|
229
|
+
`5. Or protocol becomes insolvent\n\n` +
|
|
230
|
+
`This occurred during Ethereum merge testing when stETH depegged.`,
|
|
231
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
232
|
+
line: node.loc?.start?.line || 0,
|
|
233
|
+
column: node.loc?.start?.column || 0,
|
|
234
|
+
code: funcCode.substring(0, 300),
|
|
235
|
+
severity: 'HIGH',
|
|
236
|
+
confidence: 'MEDIUM',
|
|
237
|
+
exploitable: true,
|
|
238
|
+
exploitabilityScore: 70,
|
|
239
|
+
attackVector: 'rebasing-token-vault',
|
|
240
|
+
recommendation: `1. Track user shares, not absolute amounts\n` +
|
|
241
|
+
`2. Implement pro-rata withdrawal during undercollateralization\n` +
|
|
242
|
+
`3. Add slashing buffer/insurance mechanism\n` +
|
|
243
|
+
`4. Use wstETH which abstracts rebasing complexity`,
|
|
244
|
+
references: [
|
|
245
|
+
'https://docs.lido.fi/guides/steth-integration-guide'
|
|
246
|
+
]
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check totalAssets implementation for rebasing issues
|
|
253
|
+
*/
|
|
254
|
+
checkTotalAssetsImplementation(funcCode, node) {
|
|
255
|
+
const funcName = (node.name || '').toLowerCase();
|
|
256
|
+
|
|
257
|
+
if (funcName === 'totalassets' || funcName === '_totalassets') {
|
|
258
|
+
// Check if it just returns balanceOf (vulnerable with rebasing tokens)
|
|
259
|
+
if (/return\s+\w+\.balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/i.test(funcCode)) {
|
|
260
|
+
if (this.rebasingPatterns.length > 0) {
|
|
261
|
+
this.addFinding({
|
|
262
|
+
title: 'totalAssets Uses balanceOf With Rebasing Token',
|
|
263
|
+
description: `totalAssets() returns raw balanceOf with rebasing token integration.\n\n` +
|
|
264
|
+
`Problem: Rebasing tokens change balance without transfers:\n` +
|
|
265
|
+
`- Aave aTokens: Balance increases from interest\n` +
|
|
266
|
+
`- Lido stETH: Balance changes from rewards/slashing\n` +
|
|
267
|
+
`- AMPL: Global rebases change all balances\n\n` +
|
|
268
|
+
`Impact:\n` +
|
|
269
|
+
`- Share price manipulation via rebase timing\n` +
|
|
270
|
+
`- Sandwich attacks around rebase events\n` +
|
|
271
|
+
`- First depositor attacks amplified by rebases`,
|
|
272
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
273
|
+
line: node.loc?.start?.line || 0,
|
|
274
|
+
code: funcCode.substring(0, 200),
|
|
275
|
+
severity: 'HIGH',
|
|
276
|
+
confidence: 'HIGH',
|
|
277
|
+
exploitable: true,
|
|
278
|
+
exploitabilityScore: 80,
|
|
279
|
+
attackVector: 'rebasing-token-vault',
|
|
280
|
+
recommendation: `1. Use internal accounting that tracks deposits/withdrawals\n` +
|
|
281
|
+
`2. For Aave: totalAssets = aToken.scaledBalanceOf(this) * liquidityIndex\n` +
|
|
282
|
+
`3. For Lido: Track shares instead of stETH balance\n` +
|
|
283
|
+
`4. Add virtual offset for inflation protection`
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check share calculations for rebasing-specific issues
|
|
292
|
+
*/
|
|
293
|
+
checkShareCalculation(funcCode, node) {
|
|
294
|
+
// Check for division without rebasing consideration
|
|
295
|
+
const hasDivision = /totalSupply\s*[><=!]\s*0\s*\?\s*.*\s*:\s*.*\s*\*\s*totalSupply\s*\/\s*totalAssets/i.test(funcCode) ||
|
|
296
|
+
/amount\s*\*\s*totalSupply\s*\/\s*totalAssets/i.test(funcCode);
|
|
297
|
+
|
|
298
|
+
if (hasDivision && this.rebasingPatterns.length > 0 && !this.hasRebaseProtection(funcCode)) {
|
|
299
|
+
// Already covered by deposit analysis, skip duplicate
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if function has rebasing-specific protections
|
|
305
|
+
*/
|
|
306
|
+
hasRebaseProtection(funcCode) {
|
|
307
|
+
const protections = [
|
|
308
|
+
/scaledBalance|scaledTotalSupply/i, // Aave scaled values
|
|
309
|
+
/wstETH|wrap.*stETH/i, // Lido wrapped version
|
|
310
|
+
/virtualAssets|virtualShares/i, // OZ inflation protection
|
|
311
|
+
/internalBalance|_totalDeposited/i, // Internal accounting
|
|
312
|
+
/checkpoint|snapshot/i, // Historical tracking
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
return protections.some(p => p.test(funcCode));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Analyze overall rebasing + vault interaction
|
|
320
|
+
*/
|
|
321
|
+
analyzeRebasingVaultInteractions() {
|
|
322
|
+
if (!this.isVaultContract || this.rebasingPatterns.length === 0) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if contract uses rebasing tokens without proper handling
|
|
327
|
+
const code = this.sourceCode;
|
|
328
|
+
|
|
329
|
+
// Look for dangerous combinations
|
|
330
|
+
const hasDirectBalanceOf = /\.balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/i.test(code);
|
|
331
|
+
const hasShareMath = /totalSupply\s*[/*]/i.test(code);
|
|
332
|
+
const hasProtection = this.hasRebaseProtection(code);
|
|
333
|
+
|
|
334
|
+
if (hasDirectBalanceOf && hasShareMath && !hasProtection) {
|
|
335
|
+
// General warning about rebasing integration
|
|
336
|
+
const tokenTypes = this.rebasingPatterns.map(p => p.type).join(', ');
|
|
337
|
+
|
|
338
|
+
this.addFinding({
|
|
339
|
+
title: 'Vault Uses Rebasing Tokens Without Proper Accounting',
|
|
340
|
+
description: `Contract '${this.currentContract}' appears to be a vault that integrates rebasing tokens (${tokenTypes}) ` +
|
|
341
|
+
`but uses direct balanceOf for share calculations.\n\n` +
|
|
342
|
+
`This is a critical vulnerability class that has caused multiple exploits:\n` +
|
|
343
|
+
`- Rebasing rewards get trapped in contract (not distributed to depositors)\n` +
|
|
344
|
+
`- Share price manipulation via rebase timing/MEV\n` +
|
|
345
|
+
`- First depositor attacks amplified\n` +
|
|
346
|
+
`- Potential insolvency during negative rebases (Lido slashing)`,
|
|
347
|
+
location: `Contract: ${this.currentContract}`,
|
|
348
|
+
line: 1,
|
|
349
|
+
severity: 'CRITICAL',
|
|
350
|
+
confidence: 'MEDIUM',
|
|
351
|
+
exploitable: true,
|
|
352
|
+
exploitabilityScore: 85,
|
|
353
|
+
attackVector: 'rebasing-token-vault',
|
|
354
|
+
recommendation: `CRITICAL: Review rebasing token integration:\n` +
|
|
355
|
+
`1. Never use balanceOf for share calculations with rebasing tokens\n` +
|
|
356
|
+
`2. Implement internal deposit tracking\n` +
|
|
357
|
+
`3. For Aave: Use scaledBalanceOf and rayMul with liquidityIndex\n` +
|
|
358
|
+
`4. For Lido: Use wstETH or track shares directly\n` +
|
|
359
|
+
`5. Add virtual offset (10^decimals initial shares) per OZ recommendation`,
|
|
360
|
+
references: [
|
|
361
|
+
'https://docs.aave.com/developers/guides/interest-bearing-tokens',
|
|
362
|
+
'https://docs.lido.fi/guides/steth-integration-guide',
|
|
363
|
+
'https://blog.openzeppelin.com/a-novel-defense-against-erc4626-inflation-attacks'
|
|
364
|
+
]
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
generateRebasingDepositPoC(tokenType) {
|
|
370
|
+
return `// SPDX-License-Identifier: MIT
|
|
371
|
+
pragma solidity ^0.8.0;
|
|
372
|
+
|
|
373
|
+
import "forge-std/Test.sol";
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* PoC: Rebasing Token Vault Inflation Attack
|
|
377
|
+
* Target: ${tokenType} integrated vault
|
|
378
|
+
*
|
|
379
|
+
* Demonstrates how rebasing amplifies share manipulation
|
|
380
|
+
*/
|
|
381
|
+
contract RebasingVaultExploit is Test {
|
|
382
|
+
// IVault vault;
|
|
383
|
+
// IERC20 rebasingToken; // aToken, stETH, etc.
|
|
384
|
+
|
|
385
|
+
function testRebasingInflationAttack() public {
|
|
386
|
+
// Setup: Deploy vault with ${tokenType}
|
|
387
|
+
|
|
388
|
+
// Step 1: Attacker deposits minimal amount (1 wei)
|
|
389
|
+
// uint256 attackerShares = vault.deposit(1, attacker);
|
|
390
|
+
// assert(attackerShares == 1); // Gets 1 share
|
|
391
|
+
|
|
392
|
+
// Step 2: Wait for rebase event
|
|
393
|
+
// For Aave: Interest accrues
|
|
394
|
+
// For Lido: Staking rewards distribute
|
|
395
|
+
// For AMPL: Global rebase occurs
|
|
396
|
+
// vm.warp(block.timestamp + 1 days);
|
|
397
|
+
|
|
398
|
+
// Simulate rebase: vault's balanceOf increases
|
|
399
|
+
// deal(address(rebasingToken), address(vault), 1000e18);
|
|
400
|
+
|
|
401
|
+
// Step 3: Victim deposits 500e18
|
|
402
|
+
// uint256 victimShares = vault.deposit(500e18, victim);
|
|
403
|
+
// Expected: ~500e18 shares
|
|
404
|
+
// Actual: 500e18 * 1 / 1000e18 = 0 shares (or dust)
|
|
405
|
+
|
|
406
|
+
// Step 4: Attacker withdraws all
|
|
407
|
+
// uint256 attackerAssets = vault.redeem(1, attacker, attacker);
|
|
408
|
+
// Attacker gets: ~1000e18 + 500e18 = 1500e18 assets!
|
|
409
|
+
|
|
410
|
+
// Result: Attacker stole victim's deposit via rebase timing
|
|
411
|
+
}
|
|
412
|
+
}`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = RebasingTokenVaultDetector;
|