recon-generate 0.0.26 → 0.0.28
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/dist/coverage.js +1 -42
- package/dist/dictionary.d.ts +1 -1
- package/dist/dictionary.js +4 -30
- package/dist/generator.js +18 -66
- package/dist/index.js +11 -7
- package/dist/info.d.ts +1 -0
- package/dist/info.js +43 -43
- package/dist/pathsGenerator.js +1 -42
- package/dist/utils.d.ts +34 -1
- package/dist/utils.js +82 -1
- package/package.json +1 -1
package/dist/coverage.js
CHANGED
|
@@ -85,47 +85,6 @@ const runCmd = (cmd, cwd) => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
};
|
|
88
|
-
const loadLatestSourceUnits = async (foundryRoot) => {
|
|
89
|
-
var _a;
|
|
90
|
-
const outDir = path.join(foundryRoot, '.recon', 'out');
|
|
91
|
-
const buildInfoDir = path.join(outDir, 'build-info');
|
|
92
|
-
let files = [];
|
|
93
|
-
try {
|
|
94
|
-
const entries = await fs.readdir(buildInfoDir);
|
|
95
|
-
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
96
|
-
files = await Promise.all(jsonFiles.map(async (f) => ({
|
|
97
|
-
name: f,
|
|
98
|
-
path: path.join(buildInfoDir, f),
|
|
99
|
-
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
|
|
100
|
-
})));
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
|
|
104
|
-
}
|
|
105
|
-
if (files.length === 0) {
|
|
106
|
-
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
107
|
-
}
|
|
108
|
-
const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
|
|
109
|
-
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
110
|
-
const buildInfo = JSON.parse(fileContent);
|
|
111
|
-
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
112
|
-
if (!buildOutput) {
|
|
113
|
-
throw new Error(`Build-info file ${latestFile} is missing output data.`);
|
|
114
|
-
}
|
|
115
|
-
const filteredAstData = { ...buildOutput };
|
|
116
|
-
if (filteredAstData.sources) {
|
|
117
|
-
const validSources = {};
|
|
118
|
-
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
119
|
-
const ast = content.ast || content.legacyAST || content.AST;
|
|
120
|
-
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
121
|
-
validSources[key] = content;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
filteredAstData.sources = validSources;
|
|
125
|
-
}
|
|
126
|
-
const reader = new solc_typed_ast_1.ASTReader();
|
|
127
|
-
return reader.read(filteredAstData);
|
|
128
|
-
};
|
|
129
88
|
const shouldIncludeFunction = (fnDef) => {
|
|
130
89
|
if (!fnDef.implemented)
|
|
131
90
|
return false;
|
|
@@ -204,7 +163,7 @@ const runCoverage = async (foundryRoot, foundryConfigPath, cryticName, suiteName
|
|
|
204
163
|
const outDir = path.join(foundryRoot, '.recon', 'out');
|
|
205
164
|
const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
|
|
206
165
|
await runCmd(buildCmd, foundryRoot);
|
|
207
|
-
const sourceUnits = await
|
|
166
|
+
const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot, '.recon/out');
|
|
208
167
|
if (!sourceUnits || sourceUnits.length === 0) {
|
|
209
168
|
throw new Error('No source units were produced from the Crytic build; cannot generate coverage.');
|
|
210
169
|
}
|
package/dist/dictionary.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface DictionaryOutput {
|
|
|
17
17
|
/**
|
|
18
18
|
* Run the dictionary command
|
|
19
19
|
*/
|
|
20
|
-
export declare const runDictionary: (
|
|
20
|
+
export declare const runDictionary: (foundryRoot: string, options?: {
|
|
21
21
|
outputPath?: string;
|
|
22
22
|
json?: boolean;
|
|
23
23
|
srcPrefix?: string;
|
package/dist/dictionary.js
CHANGED
|
@@ -37,32 +37,6 @@ exports.runDictionary = void 0;
|
|
|
37
37
|
const fs = __importStar(require("fs/promises"));
|
|
38
38
|
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
39
39
|
const utils_1 = require("./utils");
|
|
40
|
-
/**
|
|
41
|
-
* Load build-info from a JSON file
|
|
42
|
-
*/
|
|
43
|
-
const loadBuildInfo = async (buildInfoPath) => {
|
|
44
|
-
var _a;
|
|
45
|
-
const fileContent = await fs.readFile(buildInfoPath, 'utf-8');
|
|
46
|
-
const buildInfo = JSON.parse(fileContent);
|
|
47
|
-
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
48
|
-
if (!buildOutput) {
|
|
49
|
-
throw new Error(`Build-info file ${buildInfoPath} is missing output data.`);
|
|
50
|
-
}
|
|
51
|
-
const filteredAstData = { ...buildOutput };
|
|
52
|
-
if (filteredAstData.sources) {
|
|
53
|
-
const validSources = {};
|
|
54
|
-
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
55
|
-
const ast = content.ast || content.legacyAST || content.AST;
|
|
56
|
-
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
57
|
-
validSources[key] = content;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
filteredAstData.sources = validSources;
|
|
61
|
-
}
|
|
62
|
-
const reader = new solc_typed_ast_1.ASTReader();
|
|
63
|
-
const sourceUnits = reader.read(filteredAstData);
|
|
64
|
-
return { sourceUnits };
|
|
65
|
-
};
|
|
66
40
|
/**
|
|
67
41
|
* Check if a type string represents an address, contract, or interface type
|
|
68
42
|
*/
|
|
@@ -181,14 +155,14 @@ const findStateVarCasts = (contract, stateVarMap) => {
|
|
|
181
155
|
/**
|
|
182
156
|
* Run the dictionary command
|
|
183
157
|
*/
|
|
184
|
-
const runDictionary = async (
|
|
158
|
+
const runDictionary = async (foundryRoot, options = {}) => {
|
|
185
159
|
var _a;
|
|
186
160
|
const log = options.json ? () => { } : console.log.bind(console);
|
|
187
161
|
const srcPrefix = (_a = options.srcPrefix) !== null && _a !== void 0 ? _a : 'src/';
|
|
188
|
-
log(`[recon-generate] Loading build-info from ${
|
|
189
|
-
const { sourceUnits } = await
|
|
162
|
+
log(`[recon-generate] Loading build-info from ${foundryRoot}`);
|
|
163
|
+
const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot);
|
|
190
164
|
if (!sourceUnits || sourceUnits.length === 0) {
|
|
191
|
-
throw new Error('No source units were produced from the build
|
|
165
|
+
throw new Error('No source units were produced from the build; cannot generate dictionary.');
|
|
192
166
|
}
|
|
193
167
|
log(`[recon-generate] Found ${sourceUnits.length} source units`);
|
|
194
168
|
const output = {};
|
package/dist/generator.js
CHANGED
|
@@ -46,7 +46,6 @@ const templateManager_1 = require("./templateManager");
|
|
|
46
46
|
const processor_1 = require("./processor");
|
|
47
47
|
const types_1 = require("./types");
|
|
48
48
|
const utils_1 = require("./utils");
|
|
49
|
-
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
50
49
|
const inputParams_1 = require("./inputParams");
|
|
51
50
|
class ReconGenerator {
|
|
52
51
|
constructor(foundryRoot, options) {
|
|
@@ -546,7 +545,7 @@ class ReconGenerator {
|
|
|
546
545
|
return config;
|
|
547
546
|
}
|
|
548
547
|
async run() {
|
|
549
|
-
var _a, _b, _c
|
|
548
|
+
var _a, _b, _c;
|
|
550
549
|
await this.ensureFoundryConfigExists();
|
|
551
550
|
const outPath = this.outDir();
|
|
552
551
|
const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
|
|
@@ -568,71 +567,24 @@ class ReconGenerator {
|
|
|
568
567
|
await this.ensureBuild(); // first build to get ABIs
|
|
569
568
|
}
|
|
570
569
|
let sourceUnits = [];
|
|
571
|
-
const reader = new solc_typed_ast_1.ASTReader();
|
|
572
570
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (latestFile) {
|
|
588
|
-
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
589
|
-
const buildInfo = JSON.parse(fileContent);
|
|
590
|
-
const buildOutput = (_c = buildInfo.output) !== null && _c !== void 0 ? _c : buildInfo;
|
|
591
|
-
if (buildOutput) {
|
|
592
|
-
const filteredAstData = { ...buildOutput };
|
|
593
|
-
if (filteredAstData.sources) {
|
|
594
|
-
const validSources = {};
|
|
595
|
-
// Paths to skip - test and script directories
|
|
596
|
-
const skipPatterns = [
|
|
597
|
-
'test/',
|
|
598
|
-
'tests/',
|
|
599
|
-
'script/',
|
|
600
|
-
'scripts/',
|
|
601
|
-
'contracts/test/',
|
|
602
|
-
'contracts/script/',
|
|
603
|
-
'contracts/scripts/',
|
|
604
|
-
'forge-std/'
|
|
605
|
-
];
|
|
606
|
-
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
607
|
-
// Skip test and script files
|
|
608
|
-
const shouldSkip = skipPatterns.some(pattern => key.includes(pattern));
|
|
609
|
-
if (shouldSkip) {
|
|
610
|
-
this.logDebug('Skipping source (pattern match)', { key });
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
const ast = content.ast || content.legacyAST || content.AST;
|
|
614
|
-
if (ast && (ast.nodeType === "SourceUnit" || ast.name === "SourceUnit")) {
|
|
615
|
-
validSources[key] = content;
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
this.logDebug('Skipping source (no source unit)', { key });
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
filteredAstData.sources = validSources;
|
|
622
|
-
this.logDebug('Filtered sources for AST read', { kept: Object.keys(validSources).length, keys: Object.keys(validSources) });
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
this.logDebug('No sources present in build output');
|
|
626
|
-
}
|
|
627
|
-
try {
|
|
628
|
-
sourceUnits = reader.read(filteredAstData);
|
|
629
|
-
}
|
|
630
|
-
catch (e) {
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
571
|
+
// Paths to skip - test and script directories
|
|
572
|
+
const skipPatterns = [
|
|
573
|
+
'test/',
|
|
574
|
+
'tests/',
|
|
575
|
+
'script/',
|
|
576
|
+
'scripts/',
|
|
577
|
+
'contracts/test/',
|
|
578
|
+
'contracts/script/',
|
|
579
|
+
'contracts/scripts/',
|
|
580
|
+
'forge-std/'
|
|
581
|
+
];
|
|
582
|
+
const result = await (0, utils_1.loadLatestBuildInfo)(this.foundryRoot, '.recon/out', skipPatterns);
|
|
583
|
+
sourceUnits = result.sourceUnits;
|
|
584
|
+
this.logDebug('Loaded source units', { count: sourceUnits.length });
|
|
634
585
|
}
|
|
635
586
|
catch (e) {
|
|
587
|
+
this.logDebug('Failed to load build info', { error: String(e) });
|
|
636
588
|
}
|
|
637
589
|
// Generate mocks if requested, then rebuild to pick them up
|
|
638
590
|
let tempMockSrc = null;
|
|
@@ -687,7 +639,7 @@ class ReconGenerator {
|
|
|
687
639
|
continue;
|
|
688
640
|
}
|
|
689
641
|
const fnSet = new Set();
|
|
690
|
-
for (const fn of (
|
|
642
|
+
for (const fn of (_b = cfg.functions) !== null && _b !== void 0 ? _b : []) {
|
|
691
643
|
if (fn === null || fn === void 0 ? void 0 : fn.signature) {
|
|
692
644
|
fnSet.add(String(fn.signature));
|
|
693
645
|
}
|
|
@@ -723,7 +675,7 @@ class ReconGenerator {
|
|
|
723
675
|
this.logDebug('Wrote parameter file', { paramPath });
|
|
724
676
|
}
|
|
725
677
|
}
|
|
726
|
-
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (
|
|
678
|
+
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_c = this.options.dynamicDeploy) !== null && _c !== void 0 ? _c : new Set());
|
|
727
679
|
await tm.generateTemplates(collected.enabledContracts, collected.adminFunctions, collected.nonSeparatedFunctions, collected.separatedByContract, collected.allContractNames);
|
|
728
680
|
}
|
|
729
681
|
async listAvailable() {
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,8 @@ const link_1 = require("./link");
|
|
|
47
47
|
const link2_1 = require("./link2");
|
|
48
48
|
const sourcemap_1 = require("./sourcemap");
|
|
49
49
|
const dictionary_1 = require("./dictionary");
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
51
|
+
const packageJson = require('../package.json');
|
|
50
52
|
function parseFilter(input) {
|
|
51
53
|
if (!input)
|
|
52
54
|
return undefined;
|
|
@@ -114,6 +116,7 @@ async function main() {
|
|
|
114
116
|
const program = new commander_1.Command();
|
|
115
117
|
program
|
|
116
118
|
.name('recon-generate')
|
|
119
|
+
.version(packageJson.version, '-v, --version', 'Output the current version')
|
|
117
120
|
.description('Scaffold Recon fuzzing suite inside a Foundry project')
|
|
118
121
|
.option('-o, --output <path>', 'Custom output folder for tests (recon will be appended if missing)')
|
|
119
122
|
.option('-r, --recon <path>', 'Path to recon.json configuration')
|
|
@@ -245,21 +248,22 @@ async function main() {
|
|
|
245
248
|
});
|
|
246
249
|
});
|
|
247
250
|
program
|
|
248
|
-
.command('dictionary
|
|
251
|
+
.command('dictionary')
|
|
249
252
|
.description('Extract storage variables with contract/interface types from build-info')
|
|
250
253
|
.option('-o, --output <path>', 'Custom output path for the dictionary JSON file')
|
|
251
254
|
.option('--json', 'Output JSON to terminal (also saves to file if -o is specified)')
|
|
252
255
|
.option('--src-prefix <prefix>', 'Source directory prefix to filter (default: "src/")', 'src/')
|
|
253
|
-
.
|
|
256
|
+
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
257
|
+
.action(async function () {
|
|
254
258
|
// @ts-ignore - Commander types are complex
|
|
255
259
|
const opts = this.opts();
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
const workspaceRoot = process.cwd();
|
|
261
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
262
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
259
263
|
const outputPath = opts.output
|
|
260
|
-
? (path.isAbsolute(opts.output) ? opts.output : path.
|
|
264
|
+
? (path.isAbsolute(opts.output) ? opts.output : path.join(foundryRoot, opts.output))
|
|
261
265
|
: undefined;
|
|
262
|
-
await (0, dictionary_1.runDictionary)(
|
|
266
|
+
await (0, dictionary_1.runDictionary)(foundryRoot, {
|
|
263
267
|
outputPath,
|
|
264
268
|
json: !!opts.json,
|
|
265
269
|
srcPrefix: opts.srcPrefix,
|
package/dist/info.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ interface InfoOutput {
|
|
|
32
32
|
with_fallback: string[];
|
|
33
33
|
with_receive: string[];
|
|
34
34
|
coverage_map: Record<string, Record<string, CoverageBlock>>;
|
|
35
|
+
exclude_from_fuzzing: string[];
|
|
35
36
|
}
|
|
36
37
|
export declare const runInfo: (foundryRoot: string, contractName: string, options?: {
|
|
37
38
|
outputPath?: string;
|
package/dist/info.js
CHANGED
|
@@ -40,34 +40,10 @@ const solc_typed_ast_1 = require("solc-typed-ast");
|
|
|
40
40
|
const utils_1 = require("./utils");
|
|
41
41
|
const z3Solver_1 = require("./z3Solver");
|
|
42
42
|
const call_tree_builder_1 = require("./analyzer/call-tree-builder");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let files = [];
|
|
48
|
-
try {
|
|
49
|
-
const entries = await fs.readdir(buildInfoDir);
|
|
50
|
-
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
51
|
-
files = await Promise.all(jsonFiles.map(async (f) => ({
|
|
52
|
-
name: f,
|
|
53
|
-
path: path.join(buildInfoDir, f),
|
|
54
|
-
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
|
|
55
|
-
})));
|
|
56
|
-
}
|
|
57
|
-
catch (e) {
|
|
58
|
-
throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
|
|
59
|
-
}
|
|
60
|
-
if (files.length === 0) {
|
|
61
|
-
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
62
|
-
}
|
|
63
|
-
const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
|
|
64
|
-
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
65
|
-
const buildInfo = JSON.parse(fileContent);
|
|
66
|
-
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
67
|
-
if (!buildOutput) {
|
|
68
|
-
throw new Error(`Build-info file ${latestFile} is missing output data.`);
|
|
69
|
-
}
|
|
70
|
-
// Extract ABIs from contracts
|
|
43
|
+
/**
|
|
44
|
+
* Extract ABIs from build output
|
|
45
|
+
*/
|
|
46
|
+
const extractAbis = (buildOutput) => {
|
|
71
47
|
const contractAbis = new Map();
|
|
72
48
|
if (buildOutput.contracts) {
|
|
73
49
|
for (const [_filePath, contracts] of Object.entries(buildOutput.contracts)) {
|
|
@@ -78,20 +54,7 @@ const loadLatestBuildInfo = async (foundryRoot) => {
|
|
|
78
54
|
}
|
|
79
55
|
}
|
|
80
56
|
}
|
|
81
|
-
|
|
82
|
-
if (filteredAstData.sources) {
|
|
83
|
-
const validSources = {};
|
|
84
|
-
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
85
|
-
const ast = content.ast || content.legacyAST || content.AST;
|
|
86
|
-
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
87
|
-
validSources[key] = content;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
filteredAstData.sources = validSources;
|
|
91
|
-
}
|
|
92
|
-
const reader = new solc_typed_ast_1.ASTReader();
|
|
93
|
-
const sourceUnits = reader.read(filteredAstData);
|
|
94
|
-
return { sourceUnits, contractAbis };
|
|
57
|
+
return contractAbis;
|
|
95
58
|
};
|
|
96
59
|
const stripDataLocation = (raw) => {
|
|
97
60
|
if (!raw)
|
|
@@ -907,6 +870,39 @@ const hasReceive = (contract) => {
|
|
|
907
870
|
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true);
|
|
908
871
|
return allFunctions.some(fn => fn.kind === solc_typed_ast_1.FunctionKind.Receive);
|
|
909
872
|
};
|
|
873
|
+
/**
|
|
874
|
+
* Check if a function contains a fuzzFromHere() call
|
|
875
|
+
*/
|
|
876
|
+
const hasFuzzFromHereCall = (fnDef) => {
|
|
877
|
+
for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
|
|
878
|
+
const expr = call.vExpression;
|
|
879
|
+
if (expr instanceof solc_typed_ast_1.MemberAccess && expr.memberName === 'fuzzFromHere') {
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return false;
|
|
884
|
+
};
|
|
885
|
+
/**
|
|
886
|
+
* Collect public/external functions that contain vm.fuzzFromHere() calls
|
|
887
|
+
* These functions should be excluded from fuzzing
|
|
888
|
+
*/
|
|
889
|
+
const collectExcludeFromFuzzing = (contract) => {
|
|
890
|
+
const exclude = [];
|
|
891
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
892
|
+
for (const fnDef of allFunctions) {
|
|
893
|
+
// Only check public/external functions
|
|
894
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External && fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (hasFuzzFromHereCall(fnDef)) {
|
|
898
|
+
// Use function name (not signature) as per the expected output format
|
|
899
|
+
if (!exclude.includes(fnDef.name)) {
|
|
900
|
+
exclude.push(fnDef.name);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return exclude;
|
|
905
|
+
};
|
|
910
906
|
/**
|
|
911
907
|
* Load source file contents for source location info
|
|
912
908
|
*/
|
|
@@ -932,10 +928,11 @@ const runInfo = async (foundryRoot, contractName, options = {}) => {
|
|
|
932
928
|
if (options.json) {
|
|
933
929
|
(0, z3Solver_1.setZ3Silent)(true);
|
|
934
930
|
}
|
|
935
|
-
const { sourceUnits,
|
|
931
|
+
const { sourceUnits, buildOutput } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot);
|
|
936
932
|
if (!sourceUnits || sourceUnits.length === 0) {
|
|
937
933
|
throw new Error('No source units were produced from the build; cannot generate info.');
|
|
938
934
|
}
|
|
935
|
+
const contractAbis = extractAbis(buildOutput);
|
|
939
936
|
// Load source file contents for assert info
|
|
940
937
|
const sourceContents = await loadSourceContents(sourceUnits);
|
|
941
938
|
const allContracts = getAllContracts(sourceUnits);
|
|
@@ -956,7 +953,10 @@ const runInfo = async (foundryRoot, contractName, options = {}) => {
|
|
|
956
953
|
with_fallback: [],
|
|
957
954
|
with_receive: [],
|
|
958
955
|
coverage_map: {},
|
|
956
|
+
exclude_from_fuzzing: [],
|
|
959
957
|
};
|
|
958
|
+
// Collect functions to exclude from fuzzing (from target contract)
|
|
959
|
+
output.exclude_from_fuzzing = collectExcludeFromFuzzing(targetContract);
|
|
960
960
|
// Collect info for all related contracts
|
|
961
961
|
for (const contract of relatedContracts) {
|
|
962
962
|
// Skip abstract contracts (their functions are inherited by concrete contracts)
|
package/dist/pathsGenerator.js
CHANGED
|
@@ -61,52 +61,11 @@ const runCmd = (cmd, cwd) => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
};
|
|
64
|
-
const loadLatestSourceUnits = async (foundryRoot) => {
|
|
65
|
-
var _a;
|
|
66
|
-
const outDir = path.join(foundryRoot, '.recon', 'out');
|
|
67
|
-
const buildInfoDir = path.join(outDir, 'build-info');
|
|
68
|
-
let files = [];
|
|
69
|
-
try {
|
|
70
|
-
const entries = await fs.readdir(buildInfoDir);
|
|
71
|
-
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
72
|
-
files = await Promise.all(jsonFiles.map(async (f) => ({
|
|
73
|
-
name: f,
|
|
74
|
-
path: path.join(buildInfoDir, f),
|
|
75
|
-
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
|
|
76
|
-
})));
|
|
77
|
-
}
|
|
78
|
-
catch (e) {
|
|
79
|
-
throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
|
|
80
|
-
}
|
|
81
|
-
if (files.length === 0) {
|
|
82
|
-
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
83
|
-
}
|
|
84
|
-
const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
|
|
85
|
-
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
86
|
-
const buildInfo = JSON.parse(fileContent);
|
|
87
|
-
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
88
|
-
if (!buildOutput) {
|
|
89
|
-
throw new Error(`Build-info file ${latestFile} is missing output data.`);
|
|
90
|
-
}
|
|
91
|
-
const filteredAstData = { ...buildOutput };
|
|
92
|
-
if (filteredAstData.sources) {
|
|
93
|
-
const validSources = {};
|
|
94
|
-
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
95
|
-
const ast = content.ast || content.legacyAST || content.AST;
|
|
96
|
-
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
97
|
-
validSources[key] = content;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
filteredAstData.sources = validSources;
|
|
101
|
-
}
|
|
102
|
-
const reader = new solc_typed_ast_1.ASTReader();
|
|
103
|
-
return reader.read(filteredAstData);
|
|
104
|
-
};
|
|
105
64
|
// ==================== Main Entry ====================
|
|
106
65
|
const runPaths = async (foundryRoot, cryticName, suiteNameSnake, options = {}) => {
|
|
107
66
|
const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
|
|
108
67
|
await runCmd(buildCmd, foundryRoot);
|
|
109
|
-
const sourceUnits = await
|
|
68
|
+
const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot, '.recon/out');
|
|
110
69
|
if (!sourceUnits || sourceUnits.length === 0) {
|
|
111
70
|
throw new Error('No source units were produced from the Crytic build; cannot generate paths.');
|
|
112
71
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
|
|
1
|
+
import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, SourceUnit, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
|
|
2
2
|
import { CallType } from './types';
|
|
3
3
|
export declare function fileExists(p: string): Promise<boolean>;
|
|
4
4
|
export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
|
|
@@ -39,3 +39,36 @@ export declare function forwardLinearization(root: ContractDefinition): Contract
|
|
|
39
39
|
* contract. `current` is the contract whose code contains the `super` call.
|
|
40
40
|
*/
|
|
41
41
|
export declare function resolveSuper(root: ContractDefinition, current: ContractDefinition, signature: string): FunctionDefinition | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Result from loading build info
|
|
44
|
+
*/
|
|
45
|
+
export interface BuildInfoResult {
|
|
46
|
+
sourceUnits: SourceUnit[];
|
|
47
|
+
buildOutput: any;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse build output and return source units.
|
|
51
|
+
* This is the core parsing logic used by both loadLatestBuildInfo and loadBuildInfoFromFile.
|
|
52
|
+
*
|
|
53
|
+
* @param buildOutput - The raw build output object
|
|
54
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
55
|
+
* @returns Array of SourceUnit objects
|
|
56
|
+
*/
|
|
57
|
+
export declare const parseBuildOutput: (buildOutput: any, skipPatterns?: string[]) => SourceUnit[];
|
|
58
|
+
/**
|
|
59
|
+
* Load build info from a specific file path.
|
|
60
|
+
*
|
|
61
|
+
* @param buildInfoPath - Path to the build-info JSON file
|
|
62
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
63
|
+
* @returns Object containing sourceUnits and raw buildOutput
|
|
64
|
+
*/
|
|
65
|
+
export declare const loadBuildInfoFromFile: (buildInfoPath: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Load the latest build info from a Foundry project.
|
|
68
|
+
*
|
|
69
|
+
* @param foundryRoot - The root directory of the Foundry project
|
|
70
|
+
* @param outputDir - The output directory relative to foundryRoot (default: 'out')
|
|
71
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
72
|
+
* @returns Object containing sourceUnits and raw buildOutput
|
|
73
|
+
*/
|
|
74
|
+
export declare const loadLatestBuildInfo: (foundryRoot: string, outputDir?: string, skipPatterns?: string[]) => Promise<BuildInfoResult>;
|
package/dist/utils.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = void 0;
|
|
39
|
+
exports.loadLatestBuildInfo = exports.loadBuildInfoFromFile = exports.parseBuildOutput = exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = void 0;
|
|
40
40
|
exports.fileExists = fileExists;
|
|
41
41
|
exports.getFoundryConfigPath = getFoundryConfigPath;
|
|
42
42
|
exports.findOutputDirectory = findOutputDirectory;
|
|
@@ -436,3 +436,84 @@ function resolveSuper(root, current, signature) {
|
|
|
436
436
|
}
|
|
437
437
|
return undefined;
|
|
438
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Parse build output and return source units.
|
|
441
|
+
* This is the core parsing logic used by both loadLatestBuildInfo and loadBuildInfoFromFile.
|
|
442
|
+
*
|
|
443
|
+
* @param buildOutput - The raw build output object
|
|
444
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
445
|
+
* @returns Array of SourceUnit objects
|
|
446
|
+
*/
|
|
447
|
+
const parseBuildOutput = (buildOutput, skipPatterns = []) => {
|
|
448
|
+
const filteredAstData = { ...buildOutput };
|
|
449
|
+
if (filteredAstData.sources) {
|
|
450
|
+
const validSources = {};
|
|
451
|
+
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
452
|
+
// Skip sources matching any of the skip patterns
|
|
453
|
+
if (skipPatterns.length > 0) {
|
|
454
|
+
const shouldSkip = skipPatterns.some(pattern => key.includes(pattern));
|
|
455
|
+
if (shouldSkip) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const ast = content.ast || content.legacyAST || content.AST;
|
|
460
|
+
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
461
|
+
validSources[key] = content;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
filteredAstData.sources = validSources;
|
|
465
|
+
}
|
|
466
|
+
const reader = new solc_typed_ast_1.ASTReader();
|
|
467
|
+
return reader.read(filteredAstData);
|
|
468
|
+
};
|
|
469
|
+
exports.parseBuildOutput = parseBuildOutput;
|
|
470
|
+
/**
|
|
471
|
+
* Load build info from a specific file path.
|
|
472
|
+
*
|
|
473
|
+
* @param buildInfoPath - Path to the build-info JSON file
|
|
474
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
475
|
+
* @returns Object containing sourceUnits and raw buildOutput
|
|
476
|
+
*/
|
|
477
|
+
const loadBuildInfoFromFile = async (buildInfoPath, skipPatterns = []) => {
|
|
478
|
+
var _a;
|
|
479
|
+
const fileContent = await fs.readFile(buildInfoPath, 'utf-8');
|
|
480
|
+
const buildInfo = JSON.parse(fileContent);
|
|
481
|
+
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
482
|
+
if (!buildOutput) {
|
|
483
|
+
throw new Error(`Build-info file ${buildInfoPath} is missing output data.`);
|
|
484
|
+
}
|
|
485
|
+
const sourceUnits = (0, exports.parseBuildOutput)(buildOutput, skipPatterns);
|
|
486
|
+
return { sourceUnits, buildOutput };
|
|
487
|
+
};
|
|
488
|
+
exports.loadBuildInfoFromFile = loadBuildInfoFromFile;
|
|
489
|
+
/**
|
|
490
|
+
* Load the latest build info from a Foundry project.
|
|
491
|
+
*
|
|
492
|
+
* @param foundryRoot - The root directory of the Foundry project
|
|
493
|
+
* @param outputDir - The output directory relative to foundryRoot (default: 'out')
|
|
494
|
+
* @param skipPatterns - Optional array of patterns to skip (e.g., ['test/', 'script/'])
|
|
495
|
+
* @returns Object containing sourceUnits and raw buildOutput
|
|
496
|
+
*/
|
|
497
|
+
const loadLatestBuildInfo = async (foundryRoot, outputDir = 'out', skipPatterns = []) => {
|
|
498
|
+
const outDir = path.join(foundryRoot, outputDir);
|
|
499
|
+
const buildInfoDir = path.join(outDir, 'build-info');
|
|
500
|
+
let files = [];
|
|
501
|
+
try {
|
|
502
|
+
const entries = await fs.readdir(buildInfoDir);
|
|
503
|
+
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
504
|
+
files = await Promise.all(jsonFiles.map(async (f) => ({
|
|
505
|
+
name: f,
|
|
506
|
+
path: path.join(buildInfoDir, f),
|
|
507
|
+
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
|
|
508
|
+
})));
|
|
509
|
+
}
|
|
510
|
+
catch (e) {
|
|
511
|
+
throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
|
|
512
|
+
}
|
|
513
|
+
if (files.length === 0) {
|
|
514
|
+
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
515
|
+
}
|
|
516
|
+
const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
|
|
517
|
+
return (0, exports.loadBuildInfoFromFile)(latestFile, skipPatterns);
|
|
518
|
+
};
|
|
519
|
+
exports.loadLatestBuildInfo = loadLatestBuildInfo;
|