white-hat-scanner 1.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 +2 -0
- package/dist/analyzer.js +852 -0
- package/dist/contest.js +144 -0
- package/dist/disclosure.js +85 -0
- package/dist/discovery.js +260 -0
- package/dist/index.js +88 -0
- package/dist/notifier.js +51 -0
- package/dist/redis.js +36 -0
- package/dist/scorer.js +33 -0
- package/dist/submission.js +103 -0
- package/dist/test/smoke.js +511 -0
- package/package.json +23 -0
- package/research/bounty-economics.md +145 -0
- package/research/tooling-landscape.md +216 -0
- package/research/vuln-pattern-library.md +401 -0
- package/src/analyzer.ts +974 -0
- package/src/contest.ts +172 -0
- package/src/disclosure.ts +111 -0
- package/src/discovery.ts +297 -0
- package/src/index.ts +105 -0
- package/src/notifier.ts +58 -0
- package/src/redis.ts +31 -0
- package/src/scorer.ts +46 -0
- package/src/submission.ts +124 -0
- package/src/test/smoke.ts +457 -0
- package/system/architecture.md +488 -0
- package/system/scanner-mvp.md +305 -0
- package/targets/active-bounty-programs.md +111 -0
- package/tsconfig.json +15 -0
package/dist/scorer.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scoreFindings = void 0;
|
|
4
|
+
const RISK_SCORES = {
|
|
5
|
+
CRITICAL: 10,
|
|
6
|
+
HIGH: 7,
|
|
7
|
+
MEDIUM: 4,
|
|
8
|
+
LOW: 1,
|
|
9
|
+
UNKNOWN: 0,
|
|
10
|
+
};
|
|
11
|
+
function scoreFindings(result) {
|
|
12
|
+
const severity = RISK_SCORES[result.riskLevel] ?? 0;
|
|
13
|
+
const slitherBonus = Math.min(result.slitherFindings.length * 0.5, 3);
|
|
14
|
+
const finalScore = severity + slitherBonus;
|
|
15
|
+
// Alert rules:
|
|
16
|
+
// - Source code must have been reviewed (no speculative architecture assessments)
|
|
17
|
+
// - CRITICAL requires at least 1 Slither finding to confirm — Claude-only CRITICAL is too noisy
|
|
18
|
+
// - HIGH without Slither is acceptable (Claude can catch logic issues Slither misses)
|
|
19
|
+
const needsAlert = result.sourceAvailable &&
|
|
20
|
+
(result.riskLevel === 'HIGH' ||
|
|
21
|
+
(result.riskLevel === 'CRITICAL' && result.slitherFindings.length > 0));
|
|
22
|
+
return {
|
|
23
|
+
protocolName: result.protocolName,
|
|
24
|
+
riskLevel: result.riskLevel,
|
|
25
|
+
severity: finalScore,
|
|
26
|
+
estimatedBounty: result.estimatedBounty,
|
|
27
|
+
disclosureSummary: result.disclosureSummary,
|
|
28
|
+
slitherCount: result.slitherFindings.length,
|
|
29
|
+
needsAlert,
|
|
30
|
+
sourceAvailable: result.sourceAvailable,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
exports.scoreFindings = scoreFindings;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSubmissionDraft = exports.formatImmunefireport = void 0;
|
|
4
|
+
const redis_1 = require("./redis");
|
|
5
|
+
const NOTIFY_CHANNEL = 'cca:notify:money-brain';
|
|
6
|
+
/** Minimum bounty to bother generating an Immunefi submission draft */
|
|
7
|
+
const MIN_BOUNTY_USD = 10000;
|
|
8
|
+
/**
|
|
9
|
+
* Format an Immunefi-style bug report from the Claude analysis summary.
|
|
10
|
+
* This is a draft for human review — never auto-submitted.
|
|
11
|
+
*/
|
|
12
|
+
function slitherStatusNote(scored, status) {
|
|
13
|
+
if (scored.slitherCount > 0) {
|
|
14
|
+
return `Slither static analysis confirmed ${scored.slitherCount} HIGH/CRITICAL finding(s).`;
|
|
15
|
+
}
|
|
16
|
+
switch (status) {
|
|
17
|
+
case 'compilation_error':
|
|
18
|
+
return 'Note: Slither could not compile the contracts (likely missing compiler version or unresolved imports). Static analysis results are unavailable; the above is Claude-only analysis. A PoC is required before submission.';
|
|
19
|
+
case 'unavailable':
|
|
20
|
+
return 'Note: Slither was not available in this scan environment. Static analysis results are unavailable; the above is Claude-only analysis.';
|
|
21
|
+
case 'not_applicable':
|
|
22
|
+
return 'Note: Slither static analysis was not applicable (non-EVM chain or no source code available). The above is Claude-only analysis based on protocol architecture.';
|
|
23
|
+
case 'success':
|
|
24
|
+
default:
|
|
25
|
+
return 'Note: Slither ran successfully but found no HIGH/CRITICAL findings. The above is Claude\'s manual review.';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function formatImmunefireport(result, scored) {
|
|
29
|
+
const severity = scored.riskLevel === 'CRITICAL' ? 'Critical' : 'High';
|
|
30
|
+
const slitherNote = slitherStatusNote(scored, result.slitherStatus ?? 'not_applicable');
|
|
31
|
+
return `# Immunefi Bug Report Draft — ${result.protocolName}
|
|
32
|
+
<!-- AUTO-GENERATED DRAFT — requires human review before submission -->
|
|
33
|
+
<!-- Immunefi portal: https://immunefi.com/bug-bounty/${result.protocolName.toLowerCase().replace(/\s+/g, '-')}/information/ -->
|
|
34
|
+
|
|
35
|
+
## Severity
|
|
36
|
+
${severity}
|
|
37
|
+
|
|
38
|
+
## Affected Protocol
|
|
39
|
+
- Name: ${result.protocolName}
|
|
40
|
+
- Chain: ${result.chain ?? 'Ethereum'}
|
|
41
|
+
- TVL at time of report: ${result.tvl != null ? `$${(result.tvl / 1e6).toFixed(1)}M` : 'unknown'}
|
|
42
|
+
|
|
43
|
+
## Vulnerability Description
|
|
44
|
+
${result.disclosureSummary}
|
|
45
|
+
|
|
46
|
+
## Static Analysis
|
|
47
|
+
${slitherNote}
|
|
48
|
+
|
|
49
|
+
## Impact
|
|
50
|
+
A successful exploit could result in loss of funds or protocol compromise. Estimated maximum bounty: $${scored.estimatedBounty.toLocaleString()}.
|
|
51
|
+
|
|
52
|
+
## Proof of Concept
|
|
53
|
+
<!-- TODO: Add PoC exploit code / transaction trace here before submitting -->
|
|
54
|
+
<!-- Without a PoC, most Immunefi programs will reject the report -->
|
|
55
|
+
|
|
56
|
+
## Recommended Mitigation
|
|
57
|
+
<!-- TODO: Add specific remediation steps here -->
|
|
58
|
+
|
|
59
|
+
## References
|
|
60
|
+
- Analysis timestamp: ${new Date(result.scannedAt).toISOString()}
|
|
61
|
+
- Internal disclosure ID: ${result.protocolId}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
exports.formatImmunefireport = formatImmunefireport;
|
|
65
|
+
/**
|
|
66
|
+
* Generate and store an Immunefi submission draft for a qualifying finding.
|
|
67
|
+
* Only called for HIGH/CRITICAL findings with real source code and bounty > MIN_BOUNTY_USD.
|
|
68
|
+
* The draft is stored in Redis for human review — never auto-submitted.
|
|
69
|
+
*/
|
|
70
|
+
async function generateSubmissionDraft(result, scored) {
|
|
71
|
+
if (!scored.sourceAvailable)
|
|
72
|
+
return;
|
|
73
|
+
if (scored.riskLevel !== 'HIGH' && scored.riskLevel !== 'CRITICAL')
|
|
74
|
+
return;
|
|
75
|
+
if (scored.estimatedBounty < MIN_BOUNTY_USD)
|
|
76
|
+
return;
|
|
77
|
+
const redis = (0, redis_1.getRedis)();
|
|
78
|
+
const report = formatImmunefireport(result, scored);
|
|
79
|
+
const draft = {
|
|
80
|
+
id: `${result.protocolId}-${Date.now()}`,
|
|
81
|
+
protocolName: result.protocolName,
|
|
82
|
+
riskLevel: scored.riskLevel,
|
|
83
|
+
bounty: scored.estimatedBounty,
|
|
84
|
+
slitherCount: scored.slitherCount,
|
|
85
|
+
report,
|
|
86
|
+
createdAt: Date.now(),
|
|
87
|
+
status: 'pending_review',
|
|
88
|
+
};
|
|
89
|
+
await redis.lpush('whiteh:submissions', JSON.stringify(draft));
|
|
90
|
+
await redis.ltrim('whiteh:submissions', 0, 99);
|
|
91
|
+
// Notify for human review — include the first 400 chars of the report so the reviewer
|
|
92
|
+
// can decide whether it's worth submitting without opening Redis manually.
|
|
93
|
+
const preview = report.slice(0, 400).replace(/\n/g, ' ');
|
|
94
|
+
const notification = JSON.stringify({
|
|
95
|
+
text: `📋 IMMUNEFI DRAFT READY — ${scored.riskLevel}: ${result.protocolName}\n` +
|
|
96
|
+
`Bounty: $${(scored.estimatedBounty / 1000).toFixed(0)}k | Slither: ${scored.slitherCount} findings\n` +
|
|
97
|
+
`Preview: ${preview}\n` +
|
|
98
|
+
`Full draft: redis-cli LINDEX whiteh:submissions 0`,
|
|
99
|
+
});
|
|
100
|
+
await redis.rpush(NOTIFY_CHANNEL, notification);
|
|
101
|
+
await (0, redis_1.log)(`Immunefi submission draft created for ${result.protocolName} (${scored.riskLevel}, bounty: $${scored.estimatedBounty})`);
|
|
102
|
+
}
|
|
103
|
+
exports.generateSubmissionDraft = generateSubmissionDraft;
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const fs_1 = require("fs");
|
|
27
|
+
const path_1 = require("path");
|
|
28
|
+
const os_1 = require("os");
|
|
29
|
+
const redis_1 = require("../redis");
|
|
30
|
+
const scorer_1 = require("../scorer");
|
|
31
|
+
const disclosure_1 = require("../disclosure");
|
|
32
|
+
const discovery_1 = require("../discovery");
|
|
33
|
+
const analyzer_1 = require("../analyzer");
|
|
34
|
+
const submission_1 = require("../submission");
|
|
35
|
+
const contest_1 = require("../contest");
|
|
36
|
+
async function runSmoke() {
|
|
37
|
+
console.log('[smoke] Starting smoke tests...');
|
|
38
|
+
// Test 1: Redis connection
|
|
39
|
+
const redis = (0, redis_1.getRedis)();
|
|
40
|
+
await redis.ping();
|
|
41
|
+
console.log('[smoke] ✓ Redis connection OK');
|
|
42
|
+
// Test 2: Logging
|
|
43
|
+
await (0, redis_1.log)('smoke test log entry');
|
|
44
|
+
const logEntry = await redis.lindex('whiteh:log', 0);
|
|
45
|
+
if (!logEntry)
|
|
46
|
+
throw new Error('Log entry not written');
|
|
47
|
+
const parsed = JSON.parse(logEntry);
|
|
48
|
+
if (!parsed.message.includes('smoke test'))
|
|
49
|
+
throw new Error('Log entry malformed');
|
|
50
|
+
console.log('[smoke] ✓ Redis logging OK');
|
|
51
|
+
// Test 3: Scorer — with source code (should alert)
|
|
52
|
+
const mockResultWithCode = {
|
|
53
|
+
protocolId: 'test-protocol',
|
|
54
|
+
protocolName: 'Test Protocol',
|
|
55
|
+
chain: 'ethereum',
|
|
56
|
+
tvl: 100000000,
|
|
57
|
+
slitherFindings: [
|
|
58
|
+
{ check: 'reentrancy', impact: 'High', confidence: 'High', description: 'test', elements: [] },
|
|
59
|
+
],
|
|
60
|
+
slitherStatus: 'success',
|
|
61
|
+
claudeReview: 'RISK_LEVEL: HIGH\nBOUNTY_ESTIMATE_USD: 50000\nSUMMARY: Test summary',
|
|
62
|
+
riskLevel: 'HIGH',
|
|
63
|
+
estimatedBounty: 50000,
|
|
64
|
+
disclosureSummary: 'Test summary',
|
|
65
|
+
scannedAt: Date.now(),
|
|
66
|
+
sourceAvailable: true,
|
|
67
|
+
};
|
|
68
|
+
const scored = (0, scorer_1.scoreFindings)(mockResultWithCode);
|
|
69
|
+
if (!scored.needsAlert)
|
|
70
|
+
throw new Error('HIGH risk with source should trigger alert');
|
|
71
|
+
if (scored.severity < 7)
|
|
72
|
+
throw new Error('HIGH risk severity too low');
|
|
73
|
+
if (!scored.sourceAvailable)
|
|
74
|
+
throw new Error('sourceAvailable should be true');
|
|
75
|
+
console.log('[smoke] ✓ Scorer (with source) OK');
|
|
76
|
+
// Test 4: Scorer — no source code (should NOT alert, bounty should be 0)
|
|
77
|
+
const mockResultNoCode = {
|
|
78
|
+
...mockResultWithCode,
|
|
79
|
+
protocolId: 'test-protocol-nocode',
|
|
80
|
+
sourceAvailable: false,
|
|
81
|
+
estimatedBounty: 0,
|
|
82
|
+
};
|
|
83
|
+
const scoredNoCode = (0, scorer_1.scoreFindings)(mockResultNoCode);
|
|
84
|
+
if (scoredNoCode.needsAlert)
|
|
85
|
+
throw new Error('HIGH risk without source should NOT trigger alert');
|
|
86
|
+
if (scoredNoCode.sourceAvailable)
|
|
87
|
+
throw new Error('sourceAvailable should be false');
|
|
88
|
+
console.log('[smoke] ✓ Scorer (no source, no alert) OK');
|
|
89
|
+
// Test 5: No-source finding does NOT create a disclosure record
|
|
90
|
+
const disclosuresBefore = await redis.llen('whiteh:disclosures');
|
|
91
|
+
// Simulate what index.ts does: only call createDisclosureRecord when sourceAvailable
|
|
92
|
+
if (scoredNoCode.sourceAvailable) {
|
|
93
|
+
await (0, disclosure_1.createDisclosureRecord)(mockResultNoCode, scoredNoCode);
|
|
94
|
+
}
|
|
95
|
+
const disclosuresAfter = await redis.llen('whiteh:disclosures');
|
|
96
|
+
if (disclosuresAfter !== disclosuresBefore) {
|
|
97
|
+
throw new Error('No-source finding should not create a disclosure record');
|
|
98
|
+
}
|
|
99
|
+
console.log('[smoke] ✓ No-source finding skips disclosure record OK');
|
|
100
|
+
// Test 6: pruneNoGithubFromQueue removes low-TVL no-GitHub entries but keeps high-TVL + github ones
|
|
101
|
+
const testKey = 'whiteh:queue';
|
|
102
|
+
const noGithubLowTvl = { id: 'smoke-no-gh-low', name: 'Smoke No GitHub Low TVL', chain: 'eth', tvl: 50000000 };
|
|
103
|
+
const noGithubHighTvl = { id: 'smoke-no-gh-high', name: 'Smoke No GitHub High TVL', chain: 'eth', tvl: 600000000 };
|
|
104
|
+
const withGithub = { id: 'smoke-with-gh', name: 'Smoke With GitHub', github: 'https://github.com/test', chain: 'eth', tvl: 50000000 };
|
|
105
|
+
// Ensure clean state
|
|
106
|
+
await redis.lrem(testKey, 0, JSON.stringify(noGithubLowTvl));
|
|
107
|
+
await redis.lrem(testKey, 0, JSON.stringify(noGithubHighTvl));
|
|
108
|
+
await redis.lrem(testKey, 0, JSON.stringify(withGithub));
|
|
109
|
+
await redis.rpush(testKey, JSON.stringify(noGithubLowTvl));
|
|
110
|
+
await redis.rpush(testKey, JSON.stringify(noGithubHighTvl));
|
|
111
|
+
await redis.rpush(testKey, JSON.stringify(withGithub));
|
|
112
|
+
await (0, discovery_1.pruneNoGithubFromQueue)();
|
|
113
|
+
// noGithubLowTvl should be gone
|
|
114
|
+
const lowTvlPos = await redis.lpos(testKey, JSON.stringify(noGithubLowTvl));
|
|
115
|
+
if (lowTvlPos !== null)
|
|
116
|
+
throw new Error('Low-TVL no-GitHub entry should have been pruned');
|
|
117
|
+
// high-TVL no-GitHub should remain
|
|
118
|
+
const highTvlPos = await redis.lpos(testKey, JSON.stringify(noGithubHighTvl));
|
|
119
|
+
if (highTvlPos === null)
|
|
120
|
+
throw new Error('High-TVL no-GitHub entry should NOT have been pruned');
|
|
121
|
+
// with-GitHub should remain
|
|
122
|
+
const githubPos = await redis.lpos(testKey, JSON.stringify(withGithub));
|
|
123
|
+
if (githubPos === null)
|
|
124
|
+
throw new Error('GitHub-available entry should NOT have been pruned');
|
|
125
|
+
// clean up test entries
|
|
126
|
+
await redis.lrem(testKey, 0, JSON.stringify(noGithubHighTvl));
|
|
127
|
+
await redis.lrem(testKey, 0, JSON.stringify(withGithub));
|
|
128
|
+
console.log('[smoke] ✓ pruneNoGithubFromQueue removes low-TVL no-GitHub entries OK');
|
|
129
|
+
// Test 7: resolveCloneUrl passthrough for already-URL values
|
|
130
|
+
const fullUrl = 'https://github.com/aave/aave-v3-core';
|
|
131
|
+
const resolved = await (0, analyzer_1.resolveCloneUrl)(fullUrl);
|
|
132
|
+
if (resolved !== fullUrl)
|
|
133
|
+
throw new Error(`resolveCloneUrl should pass through full URLs unchanged, got: ${resolved}`);
|
|
134
|
+
console.log('[smoke] ✓ resolveCloneUrl passthrough OK');
|
|
135
|
+
// Test 8: resolveCloneUrl resolves org name to a valid https URL
|
|
136
|
+
const orgUrl = await (0, analyzer_1.resolveCloneUrl)('uniswap');
|
|
137
|
+
if (!orgUrl || !orgUrl.startsWith('https://github.com/uniswap/')) {
|
|
138
|
+
throw new Error(`resolveCloneUrl should resolve org to https URL, got: ${orgUrl}`);
|
|
139
|
+
}
|
|
140
|
+
console.log(`[smoke] ✓ resolveCloneUrl org resolution OK (${orgUrl})`);
|
|
141
|
+
// Test 9: Alert payload uses 'text' field (not 'message') for Telegram compatibility
|
|
142
|
+
const { sendAlert } = await Promise.resolve().then(() => __importStar(require('../notifier')));
|
|
143
|
+
// Use a unique name to avoid dedup from prior smoke runs
|
|
144
|
+
const testFinding = { ...scored, protocolName: `Smoke-Test-${Date.now()}` };
|
|
145
|
+
// Pre-clear any prior dedup entry for this name just in case
|
|
146
|
+
const alertsBefore = await redis.llen('cca:notify:money-brain');
|
|
147
|
+
await sendAlert(testFinding);
|
|
148
|
+
const alertsAfter = await redis.llen('cca:notify:money-brain');
|
|
149
|
+
if (alertsAfter <= alertsBefore)
|
|
150
|
+
throw new Error('Alert should have been pushed to notification channel');
|
|
151
|
+
const rawAlert = await redis.lindex('cca:notify:money-brain', 0);
|
|
152
|
+
if (!rawAlert)
|
|
153
|
+
throw new Error('Alert not found in channel');
|
|
154
|
+
const alertPayload = JSON.parse(rawAlert);
|
|
155
|
+
if (!alertPayload.text)
|
|
156
|
+
throw new Error(`Alert payload missing 'text' field (got keys: ${Object.keys(alertPayload).join(', ')})`);
|
|
157
|
+
if (alertPayload.message)
|
|
158
|
+
throw new Error(`Alert payload should not have 'message' field (Telegram compatibility)`);
|
|
159
|
+
// clean up test alert
|
|
160
|
+
await redis.lrem('cca:notify:money-brain', 1, rawAlert);
|
|
161
|
+
await redis.lrem('whiteh:alerts', 1, JSON.stringify({ ts: Date.now() }));
|
|
162
|
+
console.log('[smoke] ✓ Alert payload uses text field (Telegram-compatible) OK');
|
|
163
|
+
// Test 10: contractPriority filters out test/mock/interface files
|
|
164
|
+
// Test files should be filtered out (priority = 0)
|
|
165
|
+
if ((0, analyzer_1.contractPriority)('/repo/test/PoolTest.sol') !== 0)
|
|
166
|
+
throw new Error('test/ dir should be filtered');
|
|
167
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/MockToken.sol') !== 0)
|
|
168
|
+
throw new Error('Mock*.sol should be filtered');
|
|
169
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/IPool.sol') !== 0)
|
|
170
|
+
throw new Error('ICapitalized.sol should be filtered');
|
|
171
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/PoolTest.sol') !== 0)
|
|
172
|
+
throw new Error('*Test.sol should be filtered');
|
|
173
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/Pool.t.sol') !== 0)
|
|
174
|
+
throw new Error('*.t.sol should be filtered');
|
|
175
|
+
// Core contracts should have boosted priority
|
|
176
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/Pool.sol') <= 0)
|
|
177
|
+
throw new Error('Pool.sol should have positive priority');
|
|
178
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/Vault.sol') < (0, analyzer_1.contractPriority)('/repo/contracts/Library.sol')) {
|
|
179
|
+
throw new Error('Vault.sol should have higher priority than Library.sol');
|
|
180
|
+
}
|
|
181
|
+
// Regular contracts have default priority
|
|
182
|
+
if ((0, analyzer_1.contractPriority)('/repo/contracts/MyProtocol.sol') <= 0)
|
|
183
|
+
throw new Error('MyProtocol.sol should have positive priority');
|
|
184
|
+
console.log('[smoke] ✓ contractPriority filters test/mock/interface files OK');
|
|
185
|
+
// Test 11: CRITICAL without Slither should NOT alert (Claude-only CRITICAL = noisy)
|
|
186
|
+
const mockCriticalNoSlither = {
|
|
187
|
+
...mockResultWithCode,
|
|
188
|
+
protocolId: 'test-critical-no-slither',
|
|
189
|
+
protocolName: 'Test Critical No Slither',
|
|
190
|
+
slitherFindings: [],
|
|
191
|
+
slitherStatus: 'compilation_error',
|
|
192
|
+
riskLevel: 'CRITICAL',
|
|
193
|
+
estimatedBounty: 1000000,
|
|
194
|
+
};
|
|
195
|
+
const scoredCriticalNoSlither = (0, scorer_1.scoreFindings)(mockCriticalNoSlither);
|
|
196
|
+
if (scoredCriticalNoSlither.needsAlert) {
|
|
197
|
+
throw new Error('CRITICAL without Slither should NOT trigger alert (too noisy)');
|
|
198
|
+
}
|
|
199
|
+
console.log('[smoke] ✓ CRITICAL without Slither = no alert OK');
|
|
200
|
+
// Test 12: CRITICAL WITH Slither SHOULD alert
|
|
201
|
+
const mockCriticalWithSlither = {
|
|
202
|
+
...mockResultWithCode,
|
|
203
|
+
protocolId: 'test-critical-with-slither',
|
|
204
|
+
protocolName: 'Test Critical With Slither',
|
|
205
|
+
slitherFindings: [
|
|
206
|
+
{ check: 'arbitrary-send', impact: 'Critical', confidence: 'High', description: 'test', elements: [] },
|
|
207
|
+
],
|
|
208
|
+
riskLevel: 'CRITICAL',
|
|
209
|
+
estimatedBounty: 500000,
|
|
210
|
+
};
|
|
211
|
+
const scoredCriticalWithSlither = (0, scorer_1.scoreFindings)(mockCriticalWithSlither);
|
|
212
|
+
if (!scoredCriticalWithSlither.needsAlert) {
|
|
213
|
+
throw new Error('CRITICAL with Slither findings SHOULD trigger alert');
|
|
214
|
+
}
|
|
215
|
+
console.log('[smoke] ✓ CRITICAL with Slither findings = alert OK');
|
|
216
|
+
// Test 13: formatImmunefireport generates valid Immunefi-style report
|
|
217
|
+
const scoredForReport = (0, scorer_1.scoreFindings)(mockResultWithCode);
|
|
218
|
+
const report = (0, submission_1.formatImmunefireport)(mockResultWithCode, scoredForReport);
|
|
219
|
+
if (!report.includes('# Immunefi Bug Report Draft'))
|
|
220
|
+
throw new Error('Report missing header');
|
|
221
|
+
if (!report.includes('Test Protocol'))
|
|
222
|
+
throw new Error('Report missing protocol name');
|
|
223
|
+
if (!report.includes('High'))
|
|
224
|
+
throw new Error('Report missing severity');
|
|
225
|
+
if (!report.includes('AUTO-GENERATED DRAFT'))
|
|
226
|
+
throw new Error('Report missing review warning');
|
|
227
|
+
console.log('[smoke] ✓ formatImmunefireport generates valid report structure OK');
|
|
228
|
+
// Test 14: generateSubmissionDraft creates Redis entry and Telegram notification for qualifying finding
|
|
229
|
+
const submissionsBefore = await redis.llen('whiteh:submissions');
|
|
230
|
+
const notifyBefore = await redis.llen('cca:notify:money-brain');
|
|
231
|
+
await (0, submission_1.generateSubmissionDraft)(mockResultWithCode, scoredForReport);
|
|
232
|
+
const submissionsAfter = await redis.llen('whiteh:submissions');
|
|
233
|
+
const notifyAfter = await redis.llen('cca:notify:money-brain');
|
|
234
|
+
if (submissionsAfter !== submissionsBefore + 1)
|
|
235
|
+
throw new Error('generateSubmissionDraft should push to whiteh:submissions');
|
|
236
|
+
if (notifyAfter !== notifyBefore + 1)
|
|
237
|
+
throw new Error('generateSubmissionDraft should push Telegram notification');
|
|
238
|
+
// Verify the notification has the right format — rpush appends to the right end (index -1)
|
|
239
|
+
const rawNotify = await redis.lindex('cca:notify:money-brain', -1);
|
|
240
|
+
if (!rawNotify)
|
|
241
|
+
throw new Error('Submission notification not found');
|
|
242
|
+
const notifyPayload = JSON.parse(rawNotify);
|
|
243
|
+
if (!notifyPayload.text || !notifyPayload.text.includes('IMMUNEFI DRAFT READY')) {
|
|
244
|
+
throw new Error(`Submission notification missing expected text (got: ${JSON.stringify(notifyPayload)})`);
|
|
245
|
+
}
|
|
246
|
+
// clean up test entries
|
|
247
|
+
const rawSub = await redis.lindex('whiteh:submissions', 0);
|
|
248
|
+
if (rawSub)
|
|
249
|
+
await redis.lrem('whiteh:submissions', 1, rawSub);
|
|
250
|
+
await redis.lrem('cca:notify:money-brain', 1, rawNotify);
|
|
251
|
+
console.log('[smoke] ✓ generateSubmissionDraft creates submission + notification OK');
|
|
252
|
+
// Test 15: generateSubmissionDraft skips low-bounty findings
|
|
253
|
+
const lowBountyResult = { ...mockResultWithCode, estimatedBounty: 5000 };
|
|
254
|
+
const scoredLowBounty = (0, scorer_1.scoreFindings)(lowBountyResult);
|
|
255
|
+
const subsBefore = await redis.llen('whiteh:submissions');
|
|
256
|
+
await (0, submission_1.generateSubmissionDraft)(lowBountyResult, scoredLowBounty);
|
|
257
|
+
const subsAfter = await redis.llen('whiteh:submissions');
|
|
258
|
+
if (subsAfter !== subsBefore)
|
|
259
|
+
throw new Error('generateSubmissionDraft should skip bounty < $10k');
|
|
260
|
+
console.log('[smoke] ✓ generateSubmissionDraft skips low-bounty findings OK');
|
|
261
|
+
// Test 16: isExploitTarget matches known exploit targets
|
|
262
|
+
if (!(0, discovery_1.isExploitTarget)('Compound Finance'))
|
|
263
|
+
throw new Error('Compound should be an exploit target');
|
|
264
|
+
if (!(0, discovery_1.isExploitTarget)('Ronin Network'))
|
|
265
|
+
throw new Error('Ronin should be an exploit target');
|
|
266
|
+
if (!(0, discovery_1.isExploitTarget)('Curve Finance'))
|
|
267
|
+
throw new Error('Curve should be an exploit target');
|
|
268
|
+
if (!(0, discovery_1.isExploitTarget)('BadgerDAO'))
|
|
269
|
+
throw new Error('Badger should be an exploit target');
|
|
270
|
+
if ((0, discovery_1.isExploitTarget)('MyRandomProtocol'))
|
|
271
|
+
throw new Error('Random protocol should NOT be an exploit target');
|
|
272
|
+
if ((0, discovery_1.isExploitTarget)('SafeToken'))
|
|
273
|
+
throw new Error('SafeToken should NOT be an exploit target');
|
|
274
|
+
console.log('[smoke] ✓ isExploitTarget matches known exploit history protocols OK');
|
|
275
|
+
// Test 17: prioritizeExploitTargets moves exploit-history entries before normal entries.
|
|
276
|
+
// Append both to the real queue (normal first, exploit second — reversed from desired order),
|
|
277
|
+
// then verify that after reorder the exploit entry has a lower index than the normal one.
|
|
278
|
+
const exploitProto = { id: 'smoke-exploit-t17', name: 'Compound Fork Protocol', github: 'https://github.com/test/compound-fork', chain: 'eth', tvl: 50000000 };
|
|
279
|
+
const normalProto = { id: 'smoke-normal-t17', name: 'Some Normal Protocol T17', github: 'https://github.com/test/normal', chain: 'eth', tvl: 50000000 };
|
|
280
|
+
// Clean any leftover test entries
|
|
281
|
+
await redis.lrem('whiteh:queue', 0, JSON.stringify(exploitProto));
|
|
282
|
+
await redis.lrem('whiteh:queue', 0, JSON.stringify(normalProto));
|
|
283
|
+
// Insert normal first, then exploit — wrong order intentionally
|
|
284
|
+
await redis.rpush('whiteh:queue', JSON.stringify(normalProto));
|
|
285
|
+
await redis.rpush('whiteh:queue', JSON.stringify(exploitProto));
|
|
286
|
+
// Run reorder
|
|
287
|
+
await (0, discovery_1.prioritizeExploitTargets)();
|
|
288
|
+
// Check: exploit entry must appear before normal entry
|
|
289
|
+
const reorderedAll = await redis.lrange('whiteh:queue', 0, -1);
|
|
290
|
+
const exploitIdx = reorderedAll.findIndex((r) => { try {
|
|
291
|
+
return JSON.parse(r).id === 'smoke-exploit-t17';
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return false;
|
|
295
|
+
} });
|
|
296
|
+
const normalIdx = reorderedAll.findIndex((r) => { try {
|
|
297
|
+
return JSON.parse(r).id === 'smoke-normal-t17';
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return false;
|
|
301
|
+
} });
|
|
302
|
+
if (exploitIdx === -1 || normalIdx === -1)
|
|
303
|
+
throw new Error(`Test entries missing after reorder (exploit: ${exploitIdx}, normal: ${normalIdx})`);
|
|
304
|
+
if (exploitIdx >= normalIdx)
|
|
305
|
+
throw new Error(`Exploit target (idx ${exploitIdx}) should come before normal (idx ${normalIdx})`);
|
|
306
|
+
// Clean up
|
|
307
|
+
await redis.lrem('whiteh:queue', 0, JSON.stringify(exploitProto));
|
|
308
|
+
await redis.lrem('whiteh:queue', 0, JSON.stringify(normalProto));
|
|
309
|
+
console.log('[smoke] ✓ prioritizeExploitTargets moves exploit-history targets to queue front OK');
|
|
310
|
+
// Test 18: resolveCloneUrl handles GitHub org URLs (https://github.com/OrgName with no repo)
|
|
311
|
+
// DeFiLlama sometimes returns org URLs rather than plain org names — these were previously
|
|
312
|
+
// passed through as-is, causing git clone to fail on a URL that has no repo path.
|
|
313
|
+
const orgUrlResolved = await (0, analyzer_1.resolveCloneUrl)('https://github.com/uniswap');
|
|
314
|
+
if (!orgUrlResolved || !orgUrlResolved.startsWith('https://github.com/uniswap/')) {
|
|
315
|
+
throw new Error(`resolveCloneUrl should resolve org URL to repo URL, got: ${orgUrlResolved}`);
|
|
316
|
+
}
|
|
317
|
+
// Full repo URLs with 2 path segments must still pass through unchanged
|
|
318
|
+
const fullRepoUrl = 'https://github.com/aave/aave-v3-core';
|
|
319
|
+
const fullResolved = await (0, analyzer_1.resolveCloneUrl)(fullRepoUrl);
|
|
320
|
+
if (fullResolved !== fullRepoUrl) {
|
|
321
|
+
throw new Error(`resolveCloneUrl should not modify full repo URLs, got: ${fullResolved}`);
|
|
322
|
+
}
|
|
323
|
+
console.log(`[smoke] ✓ resolveCloneUrl org URL resolution OK (${orgUrlResolved})`);
|
|
324
|
+
// Test 19: queueNewProtocols uses whiteh:queued set — does NOT re-queue protocols already in set
|
|
325
|
+
const { queueNewProtocols, dequeueProtocol } = await Promise.resolve().then(() => __importStar(require('../discovery')));
|
|
326
|
+
const dedupeProto = {
|
|
327
|
+
id: `smoke-dedup-${Date.now()}`,
|
|
328
|
+
name: 'Smoke Dedup Test Protocol',
|
|
329
|
+
github: 'https://github.com/test/dedup-proto',
|
|
330
|
+
chain: 'eth',
|
|
331
|
+
tvl: 50000000,
|
|
332
|
+
};
|
|
333
|
+
// Ensure protocol is NOT already scanned
|
|
334
|
+
await redis.srem('whiteh:scanned', dedupeProto.id);
|
|
335
|
+
// Simulate it already being in the queue (add to whiteh:queued set, not the list)
|
|
336
|
+
await redis.sadd('whiteh:queued', dedupeProto.id);
|
|
337
|
+
const queueLenBefore = await redis.llen('whiteh:queue');
|
|
338
|
+
await queueNewProtocols([dedupeProto]);
|
|
339
|
+
const queueLenAfter = await redis.llen('whiteh:queue');
|
|
340
|
+
if (queueLenAfter !== queueLenBefore) {
|
|
341
|
+
throw new Error(`queueNewProtocols should skip protocols already in whiteh:queued (queue grew by ${queueLenAfter - queueLenBefore})`);
|
|
342
|
+
}
|
|
343
|
+
await redis.srem('whiteh:queued', dedupeProto.id);
|
|
344
|
+
console.log('[smoke] ✓ queueNewProtocols skips protocols in whiteh:queued set OK');
|
|
345
|
+
// Test 20: dequeueProtocol skips already-scanned entries (duplicate cleanup)
|
|
346
|
+
const skipProto = {
|
|
347
|
+
id: `smoke-skip-${Date.now()}`,
|
|
348
|
+
name: 'Smoke Skip Already Scanned',
|
|
349
|
+
github: 'https://github.com/test/skip-proto',
|
|
350
|
+
chain: 'eth',
|
|
351
|
+
tvl: 50000000,
|
|
352
|
+
};
|
|
353
|
+
const keepProto = {
|
|
354
|
+
id: `smoke-keep-${Date.now()}`,
|
|
355
|
+
name: 'Smoke Keep Unscanned',
|
|
356
|
+
github: 'https://github.com/test/keep-proto',
|
|
357
|
+
chain: 'eth',
|
|
358
|
+
tvl: 50000000,
|
|
359
|
+
};
|
|
360
|
+
// Mark skipProto as already scanned, keepProto is fresh
|
|
361
|
+
await redis.sadd('whiteh:scanned', skipProto.id);
|
|
362
|
+
await redis.srem('whiteh:scanned', keepProto.id);
|
|
363
|
+
// Push skipProto at front, keepProto behind it (skipProto is the "duplicate")
|
|
364
|
+
await redis.lpush('whiteh:queue', JSON.stringify(keepProto));
|
|
365
|
+
await redis.lpush('whiteh:queue', JSON.stringify(skipProto));
|
|
366
|
+
const dequeued = await dequeueProtocol();
|
|
367
|
+
if (!dequeued)
|
|
368
|
+
throw new Error('dequeueProtocol returned null — should have skipped skipProto and returned keepProto');
|
|
369
|
+
if (dequeued.id !== keepProto.id) {
|
|
370
|
+
throw new Error(`dequeueProtocol should skip already-scanned entry and return next, got id=${dequeued.id}`);
|
|
371
|
+
}
|
|
372
|
+
// Cleanup
|
|
373
|
+
await redis.lrem('whiteh:queue', 0, JSON.stringify(keepProto));
|
|
374
|
+
await redis.lrem('whiteh:queue', 0, JSON.stringify(skipProto));
|
|
375
|
+
await redis.srem('whiteh:scanned', skipProto.id);
|
|
376
|
+
console.log('[smoke] ✓ dequeueProtocol skips already-scanned entries OK');
|
|
377
|
+
// Test 21: parseProtocolName — Code4rena repo naming convention
|
|
378
|
+
if ((0, contest_1.parseProtocolName)('code-423n4', '2024-01-myprotocol') !== 'Myprotocol (C4)') {
|
|
379
|
+
throw new Error(`parseProtocolName C4 basic failed: got "${(0, contest_1.parseProtocolName)('code-423n4', '2024-01-myprotocol')}"`);
|
|
380
|
+
}
|
|
381
|
+
if ((0, contest_1.parseProtocolName)('code-423n4', '2024-01-uniswap-v4') !== 'Uniswap V4 (C4)') {
|
|
382
|
+
throw new Error(`parseProtocolName C4 multi-word failed: got "${(0, contest_1.parseProtocolName)('code-423n4', '2024-01-uniswap-v4')}"`);
|
|
383
|
+
}
|
|
384
|
+
console.log('[smoke] ✓ parseProtocolName Code4rena convention OK');
|
|
385
|
+
// Test 22: parseProtocolName — Sherlock repo naming convention
|
|
386
|
+
if ((0, contest_1.parseProtocolName)('sherlock-audit', '2024-curve-audit') !== 'Curve (Sherlock)') {
|
|
387
|
+
throw new Error(`parseProtocolName Sherlock basic failed: got "${(0, contest_1.parseProtocolName)('sherlock-audit', '2024-curve-audit')}"`);
|
|
388
|
+
}
|
|
389
|
+
if ((0, contest_1.parseProtocolName)('sherlock-audit', '2024-aave-v3-audit') !== 'Aave V3 (Sherlock)') {
|
|
390
|
+
throw new Error(`parseProtocolName Sherlock multi-word failed: got "${(0, contest_1.parseProtocolName)('sherlock-audit', '2024-aave-v3-audit')}"`);
|
|
391
|
+
}
|
|
392
|
+
console.log('[smoke] ✓ parseProtocolName Sherlock convention OK');
|
|
393
|
+
// Test 23: fetchContestProtocols — returns Protocol objects with correct shape
|
|
394
|
+
// (live network call; only verify shape, not exact count)
|
|
395
|
+
const contestProtocols = await (0, contest_1.fetchContestProtocols)();
|
|
396
|
+
if (!Array.isArray(contestProtocols))
|
|
397
|
+
throw new Error('fetchContestProtocols should return an array');
|
|
398
|
+
for (const p of contestProtocols) {
|
|
399
|
+
if (!p.id.startsWith('contest-'))
|
|
400
|
+
throw new Error(`Contest protocol id should start with 'contest-': ${p.id}`);
|
|
401
|
+
if (!p.github || !p.github.startsWith('https://github.com/')) {
|
|
402
|
+
throw new Error(`Contest protocol github should be a full GitHub URL: ${p.github}`);
|
|
403
|
+
}
|
|
404
|
+
if (p.tvl <= 0)
|
|
405
|
+
throw new Error(`Contest protocol TVL should be positive: ${p.tvl}`);
|
|
406
|
+
if (!p.name)
|
|
407
|
+
throw new Error(`Contest protocol name should be non-empty`);
|
|
408
|
+
}
|
|
409
|
+
console.log(`[smoke] ✓ fetchContestProtocols returns valid Protocol objects (${contestProtocols.length} found) OK`);
|
|
410
|
+
// Test 24: installDeps — no package manager files → 'no-pkg-manager'
|
|
411
|
+
const tmpEmpty = (0, path_1.join)((0, os_1.tmpdir)(), `smoke-empty-${Date.now()}`);
|
|
412
|
+
(0, fs_1.mkdirSync)(tmpEmpty, { recursive: true });
|
|
413
|
+
try {
|
|
414
|
+
const status24 = (0, analyzer_1.installDeps)(tmpEmpty);
|
|
415
|
+
if (status24 !== 'no-pkg-manager')
|
|
416
|
+
throw new Error(`Expected 'no-pkg-manager', got '${status24}'`);
|
|
417
|
+
console.log('[smoke] ✓ installDeps no-pkg-manager OK');
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
(0, fs_1.rmSync)(tmpEmpty, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
// Test 25: installDeps — foundry.toml + non-empty lib/ → 'foundry-lib-present'
|
|
423
|
+
// lib/ must have actual content (a subdirectory) to be considered populated.
|
|
424
|
+
// An empty lib/ means submodules weren't fetched and forge install should run.
|
|
425
|
+
const tmpFoundry = (0, path_1.join)((0, os_1.tmpdir)(), `smoke-foundry-${Date.now()}`);
|
|
426
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(tmpFoundry, 'lib', 'forge-std'), { recursive: true });
|
|
427
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(tmpFoundry, 'foundry.toml'), '[profile.default]\n');
|
|
428
|
+
try {
|
|
429
|
+
const status25 = (0, analyzer_1.installDeps)(tmpFoundry);
|
|
430
|
+
if (status25 !== 'foundry-lib-present')
|
|
431
|
+
throw new Error(`Expected 'foundry-lib-present', got '${status25}'`);
|
|
432
|
+
console.log('[smoke] ✓ installDeps foundry-lib-present OK');
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
(0, fs_1.rmSync)(tmpFoundry, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
// Test 26: installDeps — package.json present → should run npm and return 'npm'
|
|
438
|
+
const tmpNpm = (0, path_1.join)((0, os_1.tmpdir)(), `smoke-npm-${Date.now()}`);
|
|
439
|
+
(0, fs_1.mkdirSync)(tmpNpm, { recursive: true });
|
|
440
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(tmpNpm, 'package.json'), JSON.stringify({ name: 'test', version: '1.0.0', dependencies: {} }));
|
|
441
|
+
try {
|
|
442
|
+
const status26 = (0, analyzer_1.installDeps)(tmpNpm);
|
|
443
|
+
if (status26 !== 'npm')
|
|
444
|
+
throw new Error(`Expected 'npm', got '${status26}'`);
|
|
445
|
+
console.log('[smoke] ✓ installDeps npm install OK');
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
(0, fs_1.rmSync)(tmpNpm, { recursive: true, force: true });
|
|
449
|
+
}
|
|
450
|
+
// Test 27: slitherStatus in Immunefi report — compilation_error shows correct note
|
|
451
|
+
const compilationErrResult = {
|
|
452
|
+
...mockResultWithCode,
|
|
453
|
+
protocolId: 'test-compilation-err',
|
|
454
|
+
protocolName: 'Test Compilation Error Protocol',
|
|
455
|
+
slitherFindings: [],
|
|
456
|
+
slitherStatus: 'compilation_error',
|
|
457
|
+
};
|
|
458
|
+
const scoredCompErr = (0, scorer_1.scoreFindings)(compilationErrResult);
|
|
459
|
+
const compilationErrReport = (0, submission_1.formatImmunefireport)(compilationErrResult, scoredCompErr);
|
|
460
|
+
if (!compilationErrReport.includes('could not compile')) {
|
|
461
|
+
throw new Error(`Compilation error report should mention compilation failure, got: ${compilationErrReport.slice(0, 300)}`);
|
|
462
|
+
}
|
|
463
|
+
console.log('[smoke] ✓ slitherStatus compilation_error report note OK');
|
|
464
|
+
// Test 28: slitherStatus 'success' with 0 findings shows correct "ran but found nothing" note
|
|
465
|
+
const successZeroResult = {
|
|
466
|
+
...mockResultWithCode,
|
|
467
|
+
protocolId: 'test-success-zero',
|
|
468
|
+
protocolName: 'Test Clean Slither Protocol',
|
|
469
|
+
slitherFindings: [],
|
|
470
|
+
slitherStatus: 'success',
|
|
471
|
+
};
|
|
472
|
+
const scoredSuccessZero = (0, scorer_1.scoreFindings)(successZeroResult);
|
|
473
|
+
const successZeroReport = (0, submission_1.formatImmunefireport)(successZeroResult, scoredSuccessZero);
|
|
474
|
+
if (!successZeroReport.includes('ran successfully but found no')) {
|
|
475
|
+
throw new Error(`Success+0 findings report should mention "ran successfully", got: ${successZeroReport.slice(0, 300)}`);
|
|
476
|
+
}
|
|
477
|
+
console.log('[smoke] ✓ slitherStatus success+0 findings report note OK');
|
|
478
|
+
// Test 29: detectSolcVersion — parses pragma from a temp directory
|
|
479
|
+
const solcTestDir = (0, path_1.join)((0, os_1.tmpdir)(), `smoke-solc-${Date.now()}`);
|
|
480
|
+
(0, fs_1.mkdirSync)(solcTestDir, { recursive: true });
|
|
481
|
+
try {
|
|
482
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(solcTestDir, 'Token.sol'), `// SPDX-License-Identifier: MIT\npragma solidity =0.7.6;\ncontract Token {}\n`);
|
|
483
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(solcTestDir, 'Pool.sol'), `// SPDX-License-Identifier: MIT\npragma solidity =0.7.6;\ncontract Pool {}\n`);
|
|
484
|
+
const detected = (0, analyzer_1.detectSolcVersion)(solcTestDir);
|
|
485
|
+
if (detected !== '0.7.6')
|
|
486
|
+
throw new Error(`detectSolcVersion expected 0.7.6, got ${detected}`);
|
|
487
|
+
console.log('[smoke] ✓ detectSolcVersion parses pinned pragma OK');
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
(0, fs_1.rmSync)(solcTestDir, { recursive: true, force: true });
|
|
491
|
+
}
|
|
492
|
+
// Test 30: detectSolcVersion with caret pragma
|
|
493
|
+
const solcCaretDir = (0, path_1.join)((0, os_1.tmpdir)(), `smoke-solc-caret-${Date.now()}`);
|
|
494
|
+
(0, fs_1.mkdirSync)(solcCaretDir, { recursive: true });
|
|
495
|
+
try {
|
|
496
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(solcCaretDir, 'A.sol'), `pragma solidity ^0.8.17;\ncontract A {}\n`);
|
|
497
|
+
const detected2 = (0, analyzer_1.detectSolcVersion)(solcCaretDir);
|
|
498
|
+
if (detected2 !== '0.8.17')
|
|
499
|
+
throw new Error(`detectSolcVersion caret expected 0.8.17, got ${detected2}`);
|
|
500
|
+
console.log('[smoke] ✓ detectSolcVersion parses caret pragma OK');
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
(0, fs_1.rmSync)(solcCaretDir, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
console.log('[smoke] All smoke tests passed (30/30) ✓');
|
|
506
|
+
await (0, redis_1.closeRedis)();
|
|
507
|
+
}
|
|
508
|
+
runSmoke().catch((err) => {
|
|
509
|
+
console.error('[smoke] FAILED:', err);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
});
|