recon-generate 0.0.5 → 0.0.7
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 +17 -0
- package/dist/coverage.d.ts +1 -0
- package/dist/coverage.js +221 -0
- package/dist/generator.d.ts +1 -0
- package/dist/generator.js +136 -16
- package/dist/index.js +17 -0
- package/dist/processor.d.ts +3 -0
- package/dist/processor.js +252 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +7 -1
- package/dist/utils.d.ts +24 -0
- package/dist/utils.js +245 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ Key options:
|
|
|
32
32
|
- `--admin "A:{fnSig,fnSig}"` — mark listed functions as admin (`asAdmin`).
|
|
33
33
|
- `--mock "A,B"` — generate mocks for the listed contracts (compiled ABIs required); mocks go under `recon[-name]/mocks` and are added to targets.
|
|
34
34
|
- `--dynamic-deploy "Foo,Bar"` — keep dynamic lists for the listed contracts: deploys one instance, tracks addresses in an array, exposes `_getRandom<Contract>` and `switch<Contract>(entropy)` helpers to rotate among deployed instances.
|
|
35
|
+
- `--coverage` — emit `recon[-name]-coverage.json` with source line ranges for included contract functions.
|
|
35
36
|
- `--name <suite>` — name the suite; affects folder (`recon-<name>`), config filenames (`echidna-<name>.yaml`, `medusa-<name>.json`, `halmos-<name>.toml`), and Crytic tester/runner names.
|
|
36
37
|
- `--force` — replace existing generated suite output under `--output` (does **not** rebuild `.recon/out`).
|
|
37
38
|
- `--force-build` — delete `.recon/out` to force a fresh compile before generation.
|
|
@@ -67,6 +68,22 @@ recon-generate --mock "Morpho,Other" --force
|
|
|
67
68
|
|
|
68
69
|
# Enable dynamic deploy helpers for Foo and Bar
|
|
69
70
|
recon-generate --dynamic-deploy "Foo,Bar" --force
|
|
71
|
+
|
|
72
|
+
# Generate coverage JSON from a Crytic tester (no scaffolding)
|
|
73
|
+
recon-generate coverage --crytic-name CryticTester --name foo
|
|
74
|
+
|
|
75
|
+
# Link subcommand
|
|
76
|
+
|
|
77
|
+
After generating a suite you can update Echidna and Medusa configs with detected Foundry libraries:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
recon-generate link
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This runs `crytic-compile` with `--foundry-compile-all` to find linked libraries and rewrites:
|
|
84
|
+
|
|
85
|
+
- Echidna YAML `cryticArgs` and `deployContracts` with deterministic placeholder addresses
|
|
86
|
+
- Medusa `compilation.platformConfig.args` to include `--compile-libraries`
|
|
70
87
|
```
|
|
71
88
|
|
|
72
89
|
### Behavior
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const runCoverage: (foundryRoot: string, foundryConfigPath: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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 () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runCoverage = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs/promises"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
41
|
+
const processor_1 = require("./processor");
|
|
42
|
+
const types_1 = require("./types");
|
|
43
|
+
const utils_1 = require("./utils");
|
|
44
|
+
const isSetupName = (name) => {
|
|
45
|
+
if (!name)
|
|
46
|
+
return false;
|
|
47
|
+
return name.toLowerCase() === 'setup';
|
|
48
|
+
};
|
|
49
|
+
const stripDataLocation = (raw) => {
|
|
50
|
+
if (!raw)
|
|
51
|
+
return '';
|
|
52
|
+
const noLocation = raw
|
|
53
|
+
.replace(/\s+(storage|memory|calldata)(\s+pointer)?/g, '')
|
|
54
|
+
.replace(/\s+pointer/g, '');
|
|
55
|
+
const noPrefix = noLocation
|
|
56
|
+
.replace(/^struct\s+/, '')
|
|
57
|
+
.replace(/^enum\s+/, '')
|
|
58
|
+
.replace(/^contract\s+/, '')
|
|
59
|
+
.trim();
|
|
60
|
+
return noPrefix;
|
|
61
|
+
};
|
|
62
|
+
const signatureFromFnDef = (fnDef) => {
|
|
63
|
+
const params = fnDef.vParameters.vParameters
|
|
64
|
+
.map((p) => { var _a; return stripDataLocation((_a = p.typeString) !== null && _a !== void 0 ? _a : ''); })
|
|
65
|
+
.join(',');
|
|
66
|
+
const name = fnDef.name
|
|
67
|
+
|| (fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor
|
|
68
|
+
? 'constructor'
|
|
69
|
+
: fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback
|
|
70
|
+
? 'fallback'
|
|
71
|
+
: fnDef.kind === solc_typed_ast_1.FunctionKind.Receive
|
|
72
|
+
? 'receive'
|
|
73
|
+
: '');
|
|
74
|
+
return `${name}(${params})`;
|
|
75
|
+
};
|
|
76
|
+
const runCmd = (cmd, cwd) => {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
(0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
|
|
79
|
+
if (err) {
|
|
80
|
+
reject(new Error(stderr || err.message));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
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
|
+
const shouldIncludeFunction = (fnDef) => {
|
|
130
|
+
if (!fnDef.implemented)
|
|
131
|
+
return false;
|
|
132
|
+
if (isSetupName(fnDef.name))
|
|
133
|
+
return false;
|
|
134
|
+
if (fnDef.kind !== solc_typed_ast_1.FunctionKind.Function)
|
|
135
|
+
return false;
|
|
136
|
+
if (fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.External &&
|
|
137
|
+
fnDef.visibility !== solc_typed_ast_1.FunctionVisibility.Public) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
|
|
141
|
+
fnDef.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
};
|
|
146
|
+
const addFunction = (map, contractName, fnDef) => {
|
|
147
|
+
var _a;
|
|
148
|
+
const sig = signatureFromFnDef(fnDef);
|
|
149
|
+
if (!sig)
|
|
150
|
+
return;
|
|
151
|
+
const existing = (_a = map.get(contractName)) !== null && _a !== void 0 ? _a : new Set();
|
|
152
|
+
existing.add(sig);
|
|
153
|
+
map.set(contractName, existing);
|
|
154
|
+
};
|
|
155
|
+
const collectContractFunctions = (sourceUnits, cryticName) => {
|
|
156
|
+
var _a;
|
|
157
|
+
const functionsByContract = new Map();
|
|
158
|
+
const contracts = [];
|
|
159
|
+
for (const unit of sourceUnits) {
|
|
160
|
+
for (const contract of unit.getChildrenByType(solc_typed_ast_1.ContractDefinition)) {
|
|
161
|
+
if (contract.name === cryticName) {
|
|
162
|
+
contracts.push(contract);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (contracts.length === 0) {
|
|
167
|
+
throw new Error(`Contract ${cryticName} not found in build output.`);
|
|
168
|
+
}
|
|
169
|
+
const contract = contracts[0];
|
|
170
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
171
|
+
for (const fnDef of allFunctions) {
|
|
172
|
+
if (!shouldIncludeFunction(fnDef)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
addFunction(functionsByContract, contract.name, fnDef);
|
|
176
|
+
for (const call of fnDef.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
|
|
177
|
+
const callType = (0, utils_1.getCallType)(call);
|
|
178
|
+
if (callType === types_1.CallType.Internal) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const ref = (_a = call.vExpression.vReferencedDeclaration) !== null && _a !== void 0 ? _a : call.vReferencedDeclaration;
|
|
182
|
+
if (!(ref instanceof solc_typed_ast_1.FunctionDefinition)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const targetContract = ref.getClosestParentByType(solc_typed_ast_1.ContractDefinition);
|
|
186
|
+
if (!targetContract) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
|
|
190
|
+
ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (ref.kind === solc_typed_ast_1.FunctionKind.Constructor ||
|
|
194
|
+
ref.kind === solc_typed_ast_1.FunctionKind.Fallback ||
|
|
195
|
+
ref.kind === solc_typed_ast_1.FunctionKind.Receive) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
addFunction(functionsByContract, targetContract.name, ref);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return functionsByContract;
|
|
202
|
+
};
|
|
203
|
+
const runCoverage = async (foundryRoot, foundryConfigPath, cryticName, suiteNameSnake) => {
|
|
204
|
+
const outDir = path.join(foundryRoot, '.recon', 'out');
|
|
205
|
+
const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
|
|
206
|
+
await runCmd(buildCmd, foundryRoot);
|
|
207
|
+
const sourceUnits = await loadLatestSourceUnits(foundryRoot);
|
|
208
|
+
if (!sourceUnits || sourceUnits.length === 0) {
|
|
209
|
+
throw new Error('No source units were produced from the Crytic build; cannot generate coverage.');
|
|
210
|
+
}
|
|
211
|
+
const contractFunctions = collectContractFunctions(sourceUnits, cryticName);
|
|
212
|
+
if (contractFunctions.size === 0) {
|
|
213
|
+
throw new Error('No eligible functions found to include in coverage.');
|
|
214
|
+
}
|
|
215
|
+
const coverage = await (0, processor_1.buildCoverageMap)(sourceUnits, foundryRoot, contractFunctions);
|
|
216
|
+
const coverageName = suiteNameSnake ? `recon-${suiteNameSnake}-coverage.json` : 'recon-coverage.json';
|
|
217
|
+
const coveragePath = path.join(foundryRoot, coverageName);
|
|
218
|
+
await fs.writeFile(coveragePath, JSON.stringify(coverage, null, 2));
|
|
219
|
+
console.log(`[recon-generate] Wrote coverage file to ${coveragePath}`);
|
|
220
|
+
};
|
|
221
|
+
exports.runCoverage = runCoverage;
|
package/dist/generator.d.ts
CHANGED
package/dist/generator.js
CHANGED
|
@@ -43,8 +43,10 @@ const path = __importStar(require("path"));
|
|
|
43
43
|
const abi_to_mock_1 = __importDefault(require("abi-to-mock"));
|
|
44
44
|
const parser_1 = __importDefault(require("@solidity-parser/parser"));
|
|
45
45
|
const templateManager_1 = require("./templateManager");
|
|
46
|
+
const processor_1 = require("./processor");
|
|
46
47
|
const types_1 = require("./types");
|
|
47
48
|
const utils_1 = require("./utils");
|
|
49
|
+
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
48
50
|
class ReconGenerator {
|
|
49
51
|
constructor(foundryRoot, options) {
|
|
50
52
|
this.foundryRoot = foundryRoot;
|
|
@@ -109,14 +111,14 @@ class ReconGenerator {
|
|
|
109
111
|
if (await (0, utils_1.fileExists)(chimeraPath)) {
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
|
-
await this.runCmd('forge install Recon-Fuzz/chimera
|
|
114
|
+
await this.runCmd('forge install Recon-Fuzz/chimera', this.foundryRoot);
|
|
113
115
|
}
|
|
114
116
|
async installSetupHelpers() {
|
|
115
117
|
const setupPath = path.join(this.foundryRoot, 'lib', 'setup-helpers');
|
|
116
118
|
if (await (0, utils_1.fileExists)(setupPath)) {
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
119
|
-
await this.runCmd('forge install Recon-Fuzz/setup-helpers
|
|
121
|
+
await this.runCmd('forge install Recon-Fuzz/setup-helpers', this.foundryRoot);
|
|
120
122
|
}
|
|
121
123
|
async updateRemappings() {
|
|
122
124
|
const remappingsPath = path.join(this.foundryRoot, 'remappings.txt');
|
|
@@ -472,7 +474,7 @@ class ReconGenerator {
|
|
|
472
474
|
return config;
|
|
473
475
|
}
|
|
474
476
|
async run() {
|
|
475
|
-
var _a, _b;
|
|
477
|
+
var _a, _b, _c, _d, _e;
|
|
476
478
|
await this.ensureFoundryConfigExists();
|
|
477
479
|
const outPath = this.outDir();
|
|
478
480
|
const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
|
|
@@ -493,6 +495,73 @@ class ReconGenerator {
|
|
|
493
495
|
}
|
|
494
496
|
await this.ensureBuild(); // first build to get ABIs
|
|
495
497
|
}
|
|
498
|
+
let sourceUnits = [];
|
|
499
|
+
const reader = new solc_typed_ast_1.ASTReader();
|
|
500
|
+
try {
|
|
501
|
+
const buildInfoDir = path.join(this.outDir(), 'build-info');
|
|
502
|
+
this.logDebug('Scanning build-info directory', { buildInfoDir });
|
|
503
|
+
const allFiles = await fs.readdir(buildInfoDir);
|
|
504
|
+
const jsonFiles = allFiles.filter(f => f.endsWith('.json'));
|
|
505
|
+
const filesWithStats = await Promise.all(jsonFiles.map(async (f) => ({
|
|
506
|
+
name: f,
|
|
507
|
+
path: path.join(buildInfoDir, f),
|
|
508
|
+
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime
|
|
509
|
+
})));
|
|
510
|
+
const files = filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
511
|
+
if (files.length === 0) {
|
|
512
|
+
console.warn('❌ No build-info JSON files found');
|
|
513
|
+
}
|
|
514
|
+
const latestFile = (_b = files[0]) === null || _b === void 0 ? void 0 : _b.path;
|
|
515
|
+
if (latestFile) {
|
|
516
|
+
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
517
|
+
const buildInfo = JSON.parse(fileContent);
|
|
518
|
+
const buildOutput = (_c = buildInfo.output) !== null && _c !== void 0 ? _c : buildInfo;
|
|
519
|
+
if (buildOutput) {
|
|
520
|
+
const filteredAstData = { ...buildOutput };
|
|
521
|
+
if (filteredAstData.sources) {
|
|
522
|
+
const validSources = {};
|
|
523
|
+
// Paths to skip - test and script directories
|
|
524
|
+
const skipPatterns = [
|
|
525
|
+
'test/',
|
|
526
|
+
'tests/',
|
|
527
|
+
'script/',
|
|
528
|
+
'scripts/',
|
|
529
|
+
'contracts/test/',
|
|
530
|
+
'contracts/script/',
|
|
531
|
+
'contracts/scripts/',
|
|
532
|
+
'forge-std/'
|
|
533
|
+
];
|
|
534
|
+
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
535
|
+
// Skip test and script files
|
|
536
|
+
const shouldSkip = skipPatterns.some(pattern => key.includes(pattern));
|
|
537
|
+
if (shouldSkip) {
|
|
538
|
+
this.logDebug('Skipping source (pattern match)', { key });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const ast = content.ast || content.legacyAST || content.AST;
|
|
542
|
+
if (ast && (ast.nodeType === "SourceUnit" || ast.name === "SourceUnit")) {
|
|
543
|
+
validSources[key] = content;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
this.logDebug('Skipping source (no source unit)', { key });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
filteredAstData.sources = validSources;
|
|
550
|
+
this.logDebug('Filtered sources for AST read', { kept: Object.keys(validSources).length, keys: Object.keys(validSources) });
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
this.logDebug('No sources present in build output');
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
sourceUnits = reader.read(filteredAstData);
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
}
|
|
496
565
|
// Generate mocks if requested, then rebuild to pick them up
|
|
497
566
|
let tempMockSrc = null;
|
|
498
567
|
if (mockTargets.size > 0) {
|
|
@@ -511,12 +580,63 @@ class ReconGenerator {
|
|
|
511
580
|
await this.copyAndCleanupMocks(tempMockSrc);
|
|
512
581
|
}
|
|
513
582
|
const contracts = await this.findSourceContracts();
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
583
|
+
if (this.options.include) {
|
|
584
|
+
const requested = new Set([
|
|
585
|
+
...this.options.include.contractOnly,
|
|
586
|
+
...Array.from(this.options.include.functions.keys()),
|
|
587
|
+
]);
|
|
588
|
+
const discovered = new Set(contracts.map(c => c.name));
|
|
589
|
+
const missing = Array.from(requested).filter((n) => !discovered.has(n));
|
|
590
|
+
if (missing.length > 0) {
|
|
591
|
+
console.warn('[recon-generate] Include requested contracts not found in build:', missing);
|
|
592
|
+
}
|
|
517
593
|
}
|
|
594
|
+
const filteredContracts = [];
|
|
595
|
+
const skippedContracts = [];
|
|
596
|
+
for (const c of contracts) {
|
|
597
|
+
if (this.isContractAllowed(c.name)) {
|
|
598
|
+
filteredContracts.push(c);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
skippedContracts.push({ name: c.name, reason: 'filtered by include/exclude/mocks' });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
this.logDebug('Contracts after filters', { count: filteredContracts.length });
|
|
518
605
|
const reconConfig = await this.loadReconConfig(filteredContracts);
|
|
519
|
-
|
|
606
|
+
if (this.options.coverage) {
|
|
607
|
+
if (!sourceUnits || sourceUnits.length === 0) {
|
|
608
|
+
console.warn('Coverage requested but no source units were available; skipping coverage file.');
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const contractFunctions = new Map();
|
|
612
|
+
for (const contract of filteredContracts) {
|
|
613
|
+
const cfg = reconConfig[contract.jsonPath];
|
|
614
|
+
if (!cfg || cfg.enabled === false) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const fnSet = new Set();
|
|
618
|
+
for (const fn of (_d = cfg.functions) !== null && _d !== void 0 ? _d : []) {
|
|
619
|
+
if (fn === null || fn === void 0 ? void 0 : fn.signature) {
|
|
620
|
+
fnSet.add(String(fn.signature));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (fnSet.size > 0) {
|
|
624
|
+
this.logDebug('Coverage: adding contract functions', { contract: contract.name, count: fnSet.size });
|
|
625
|
+
contractFunctions.set(contract.name, fnSet);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (contractFunctions.size > 0) {
|
|
629
|
+
const coverage = await (0, processor_1.buildCoverageMap)(sourceUnits, this.foundryRoot, contractFunctions);
|
|
630
|
+
const coverageName = this.options.suiteNameSnake
|
|
631
|
+
? `recon-${this.options.suiteNameSnake}-coverage.json`
|
|
632
|
+
: 'recon-coverage.json';
|
|
633
|
+
const coveragePath = path.join(this.foundryRoot, coverageName);
|
|
634
|
+
await fs.writeFile(coveragePath, JSON.stringify(coverage, null, 2));
|
|
635
|
+
this.logDebug('Wrote coverage file', { coveragePath, files: Object.keys(coverage).length });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_e = this.options.dynamicDeploy) !== null && _e !== void 0 ? _e : new Set());
|
|
520
640
|
await tm.generateTemplates(filteredContracts, reconConfig);
|
|
521
641
|
}
|
|
522
642
|
async listAvailable() {
|
|
@@ -541,16 +661,16 @@ class ReconGenerator {
|
|
|
541
661
|
if (fns.length > 0) {
|
|
542
662
|
filtered.push({ name: contract.name, path: contract.path, functions: fns });
|
|
543
663
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
664
|
+
console.log('Available contracts and functions (after filters):');
|
|
665
|
+
for (const c of filtered) {
|
|
666
|
+
console.log(`- ${c.name} :: ${c.path}`);
|
|
667
|
+
for (const fn of c.functions) {
|
|
668
|
+
console.log(` • ${fn}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (filtered.length === 0) {
|
|
672
|
+
console.log('No contracts/functions match the current include/exclude filters.');
|
|
550
673
|
}
|
|
551
|
-
}
|
|
552
|
-
if (filtered.length === 0) {
|
|
553
|
-
console.log('No contracts/functions match the current include/exclude filters.');
|
|
554
674
|
}
|
|
555
675
|
}
|
|
556
676
|
}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ const commander_1 = require("commander");
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const case_1 = require("case");
|
|
40
40
|
const generator_1 = require("./generator");
|
|
41
|
+
const coverage_1 = require("./coverage");
|
|
41
42
|
const utils_1 = require("./utils");
|
|
42
43
|
const link_1 = require("./link");
|
|
43
44
|
function parseFilter(input) {
|
|
@@ -117,10 +118,25 @@ async function main() {
|
|
|
117
118
|
.option('--mock <names>', 'Comma-separated contract names to generate mocks for')
|
|
118
119
|
.option('--dynamic-deploy <names>', 'Comma-separated contract names to enable dynamic deploy lists')
|
|
119
120
|
.option('--debug', 'Print debug info about filters and selection')
|
|
121
|
+
.option('--coverage', 'Write coverage information to recon-coverage.json (or recon-<name>-coverage.json)')
|
|
120
122
|
.option('--list', 'List available contracts/functions (after filters) and exit')
|
|
121
123
|
.option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
|
|
122
124
|
.option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
|
|
123
125
|
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)');
|
|
126
|
+
program
|
|
127
|
+
.command('coverage')
|
|
128
|
+
.description('Generate recon-coverage.json from a Crytic tester contract without scaffolding tests')
|
|
129
|
+
.option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
|
|
130
|
+
.option('--name <suite>', 'Suite name; affects coverage filename (recon-<name>-coverage.json)')
|
|
131
|
+
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
132
|
+
.action(async (opts) => {
|
|
133
|
+
const workspaceRoot = process.cwd();
|
|
134
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
135
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
136
|
+
const suiteRaw = opts.name ? String(opts.name).trim() : '';
|
|
137
|
+
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
138
|
+
await (0, coverage_1.runCoverage)(foundryRoot, foundryConfig, opts.cryticName || 'CryticTester', suiteSnake);
|
|
139
|
+
});
|
|
124
140
|
program
|
|
125
141
|
.command('link')
|
|
126
142
|
.description('Link library addresses into echidna/medusa configs via crytic-compile')
|
|
@@ -193,6 +209,7 @@ async function main() {
|
|
|
193
209
|
dynamicDeploy: dynamicDeploySet,
|
|
194
210
|
suiteNameSnake: suiteSnake,
|
|
195
211
|
suiteNamePascal: suitePascal,
|
|
212
|
+
coverage: !!opts.coverage,
|
|
196
213
|
});
|
|
197
214
|
if (opts.list) {
|
|
198
215
|
await generator.listAvailable();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import * as $ from 'solc-typed-ast';
|
|
2
|
+
export declare const processContract: (contract: $.ContractDefinition) => Record<string, any>;
|
|
3
|
+
export declare function buildCoverageMap(asts: $.SourceUnit[], foundryRoot: string, contractFunctions: Map<string, Set<string>>): Promise<Record<string, string[]>>;
|
|
@@ -0,0 +1,252 @@
|
|
|
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 () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.processContract = void 0;
|
|
37
|
+
exports.buildCoverageMap = buildCoverageMap;
|
|
38
|
+
const fs = __importStar(require("fs/promises"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const $ = __importStar(require("solc-typed-ast"));
|
|
41
|
+
const types_1 = require("./types");
|
|
42
|
+
const utils_1 = require("./utils");
|
|
43
|
+
const stripDataLocation = (raw) => {
|
|
44
|
+
if (!raw) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
const noLocation = raw
|
|
48
|
+
.replace(/\s+(storage|memory|calldata)(\s+pointer)?/g, '')
|
|
49
|
+
.replace(/\s+pointer/g, '');
|
|
50
|
+
const noPrefix = noLocation
|
|
51
|
+
.replace(/^struct\s+/, '')
|
|
52
|
+
.replace(/^enum\s+/, '')
|
|
53
|
+
.replace(/^contract\s+/, '')
|
|
54
|
+
.trim();
|
|
55
|
+
return noPrefix;
|
|
56
|
+
};
|
|
57
|
+
const simplifyParamType = (t) => {
|
|
58
|
+
const arraySuffixMatch = t.match(/(\[.*\])$/);
|
|
59
|
+
const arraySuffix = arraySuffixMatch ? arraySuffixMatch[1] : '';
|
|
60
|
+
const base = arraySuffix ? t.slice(0, -arraySuffix.length) : t;
|
|
61
|
+
const segments = base.split('.');
|
|
62
|
+
const simple = segments[segments.length - 1];
|
|
63
|
+
return `${simple}${arraySuffix}`;
|
|
64
|
+
};
|
|
65
|
+
const simplifySignature = (sig) => {
|
|
66
|
+
const match = sig.match(/^(.*?)\((.*)\)$/);
|
|
67
|
+
if (!match)
|
|
68
|
+
return sig;
|
|
69
|
+
const name = match[1];
|
|
70
|
+
const params = match[2];
|
|
71
|
+
if (!params)
|
|
72
|
+
return `${name}()`;
|
|
73
|
+
const simplifiedParams = params
|
|
74
|
+
.split(',')
|
|
75
|
+
.map((p) => simplifyParamType(p.trim()))
|
|
76
|
+
.join(',');
|
|
77
|
+
return `${name}(${simplifiedParams})`;
|
|
78
|
+
};
|
|
79
|
+
const signatureFromFnDef = (fnDef) => {
|
|
80
|
+
const params = fnDef.vParameters.vParameters
|
|
81
|
+
.map((p) => { var _a; return stripDataLocation((_a = p.typeString) !== null && _a !== void 0 ? _a : ''); })
|
|
82
|
+
.join(',');
|
|
83
|
+
const name = fnDef.name
|
|
84
|
+
|| (fnDef.kind === $.FunctionKind.Constructor
|
|
85
|
+
? 'constructor'
|
|
86
|
+
: fnDef.kind === $.FunctionKind.Fallback
|
|
87
|
+
? 'fallback'
|
|
88
|
+
: fnDef.kind === $.FunctionKind.Receive
|
|
89
|
+
? 'receive'
|
|
90
|
+
: '');
|
|
91
|
+
return `${name}(${params})`;
|
|
92
|
+
};
|
|
93
|
+
const matchesSignature = (sig, allowed) => {
|
|
94
|
+
if (allowed.has(sig))
|
|
95
|
+
return true;
|
|
96
|
+
const nameOnly = sig.split('(')[0];
|
|
97
|
+
if (allowed.has(nameOnly))
|
|
98
|
+
return true;
|
|
99
|
+
const simplifiedSig = simplifySignature(sig);
|
|
100
|
+
const simplifiedName = simplifiedSig.split('(')[0];
|
|
101
|
+
if (allowed.has(simplifiedSig) || allowed.has(simplifiedName))
|
|
102
|
+
return true;
|
|
103
|
+
const simplifiedAllowed = new Set(Array.from(allowed).map((s) => simplifySignature(s)));
|
|
104
|
+
if (simplifiedAllowed.has(sig) || simplifiedAllowed.has(nameOnly))
|
|
105
|
+
return true;
|
|
106
|
+
if (simplifiedAllowed.has(simplifiedSig) || simplifiedAllowed.has(simplifiedName))
|
|
107
|
+
return true;
|
|
108
|
+
return false;
|
|
109
|
+
};
|
|
110
|
+
const processFunction = (fnDef, includeDeps = false) => {
|
|
111
|
+
const result = [];
|
|
112
|
+
fnDef.walk((n) => {
|
|
113
|
+
if ('vReferencedDeclaration' in n &&
|
|
114
|
+
n.vReferencedDeclaration &&
|
|
115
|
+
n.vReferencedDeclaration !== fnDef) {
|
|
116
|
+
if (n.vReferencedDeclaration instanceof $.FunctionDefinition ||
|
|
117
|
+
n.vReferencedDeclaration instanceof $.ModifierDefinition) {
|
|
118
|
+
const refSourceUnit = n.vReferencedDeclaration.getClosestParentByType($.SourceUnit);
|
|
119
|
+
if (result.some((x) => x.ast === n.vReferencedDeclaration)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
result.push({
|
|
123
|
+
ast: n.vReferencedDeclaration,
|
|
124
|
+
children: processFunction(n.vReferencedDeclaration, includeDeps),
|
|
125
|
+
callType: n instanceof $.FunctionCall ? (0, utils_1.getCallType)(n) : types_1.CallType.Internal,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
const processContract = (contract) => {
|
|
133
|
+
const result = {};
|
|
134
|
+
result['vFunctions'] = [];
|
|
135
|
+
const allFunctions = (0, utils_1.getDefinitions)(contract, 'vFunctions', true).reverse();
|
|
136
|
+
for (const fnDef of allFunctions.filter((x) => x.implemented)) {
|
|
137
|
+
if (fnDef.visibility !== $.FunctionVisibility.External &&
|
|
138
|
+
fnDef.visibility !== $.FunctionVisibility.Public) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if ((fnDef.stateMutability === $.FunctionStateMutability.Pure ||
|
|
142
|
+
fnDef.stateMutability === $.FunctionStateMutability.View)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!result['vFunctions'].some((x) => (0, utils_1.signatureEquals)(x, fnDef))) {
|
|
146
|
+
const rec = {
|
|
147
|
+
ast: fnDef,
|
|
148
|
+
children: processFunction(fnDef),
|
|
149
|
+
};
|
|
150
|
+
result['vFunctions'].push(rec);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
};
|
|
155
|
+
exports.processContract = processContract;
|
|
156
|
+
const skipPatterns = [
|
|
157
|
+
'test/',
|
|
158
|
+
'tests/',
|
|
159
|
+
'script/',
|
|
160
|
+
'scripts/',
|
|
161
|
+
'contracts/test/',
|
|
162
|
+
'contracts/script/',
|
|
163
|
+
'contracts/scripts/',
|
|
164
|
+
'lib/forge-std/',
|
|
165
|
+
'lib/chimera/',
|
|
166
|
+
'lib/setup-helpers/',
|
|
167
|
+
];
|
|
168
|
+
async function buildCoverageMap(asts, foundryRoot, contractFunctions) {
|
|
169
|
+
const coverage = new Map();
|
|
170
|
+
const fileCache = new Map();
|
|
171
|
+
let nodesVisited = 0;
|
|
172
|
+
let nodesAdded = 0;
|
|
173
|
+
const shouldSkipPath = (relPath) => {
|
|
174
|
+
return skipPatterns.some((p) => relPath.includes(p));
|
|
175
|
+
};
|
|
176
|
+
const addNodeRange = async (node) => {
|
|
177
|
+
var _a;
|
|
178
|
+
nodesVisited++;
|
|
179
|
+
const srcUnit = node.getClosestParentByType($.SourceUnit);
|
|
180
|
+
if (!srcUnit) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const parentContract = node.getClosestParentByType($.ContractDefinition);
|
|
184
|
+
if (parentContract && parentContract.kind === 'interface') {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const absPath = path.isAbsolute(srcUnit.absolutePath)
|
|
188
|
+
? srcUnit.absolutePath
|
|
189
|
+
: path.join(foundryRoot, srcUnit.absolutePath);
|
|
190
|
+
let content = fileCache.get(absPath);
|
|
191
|
+
if (!content) {
|
|
192
|
+
try {
|
|
193
|
+
content = await fs.readFile(absPath, 'utf8');
|
|
194
|
+
fileCache.set(absPath, content);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const src = node.src;
|
|
201
|
+
if (!src) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const { start, end } = (0, utils_1.getLines)(content, src);
|
|
205
|
+
const key = path.relative(foundryRoot, absPath);
|
|
206
|
+
const entry = (_a = coverage.get(key)) !== null && _a !== void 0 ? _a : new Set();
|
|
207
|
+
entry.add(start === end ? `${start}` : `${start}-${end}`);
|
|
208
|
+
coverage.set(key, entry);
|
|
209
|
+
nodesAdded++;
|
|
210
|
+
};
|
|
211
|
+
const walkRecord = async (rec) => {
|
|
212
|
+
await addNodeRange(rec.ast);
|
|
213
|
+
for (const child of rec.children) {
|
|
214
|
+
await walkRecord(child);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
for (const ast of asts) {
|
|
218
|
+
for (const contract of ast.getChildrenByType($.ContractDefinition)) {
|
|
219
|
+
if (contract.kind === 'interface') {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const allowedFns = contractFunctions.get(contract.name);
|
|
223
|
+
if (!allowedFns || allowedFns.size === 0) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const processed = (0, exports.processContract)(contract);
|
|
227
|
+
const recs = (processed['vFunctions'] || []);
|
|
228
|
+
if (recs.length === 0) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
for (const rec of recs) {
|
|
232
|
+
const sig = signatureFromFnDef(rec.ast);
|
|
233
|
+
if (!matchesSignature(sig, allowedFns)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
await walkRecord(rec);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const normalized = {};
|
|
241
|
+
for (const [relPath, ranges] of coverage.entries()) {
|
|
242
|
+
if (shouldSkipPath(relPath)) {
|
|
243
|
+
continue; // skip test/script/lib files at the final emission step only
|
|
244
|
+
}
|
|
245
|
+
const sorted = Array.from(ranges).sort((a, b) => {
|
|
246
|
+
const start = (val) => parseInt(val.split('-')[0], 10);
|
|
247
|
+
return start(a) - start(b);
|
|
248
|
+
});
|
|
249
|
+
normalized[relPath] = sorted;
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ASTNode } from "solc-typed-ast";
|
|
1
2
|
export declare enum Actor {
|
|
2
3
|
ACTOR = "actor",
|
|
3
4
|
ADMIN = "admin"
|
|
@@ -43,3 +44,13 @@ export interface FunctionDefinitionParams {
|
|
|
43
44
|
mode: Mode;
|
|
44
45
|
separated?: boolean;
|
|
45
46
|
}
|
|
47
|
+
export declare enum CallType {
|
|
48
|
+
Internal = "internal",
|
|
49
|
+
HighLevel = "high-level",
|
|
50
|
+
LowLevel = "low-level"
|
|
51
|
+
}
|
|
52
|
+
export type RecordItem = {
|
|
53
|
+
ast: ASTNode;
|
|
54
|
+
children: RecordItem[];
|
|
55
|
+
callType?: CallType;
|
|
56
|
+
};
|
package/dist/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Mode = exports.Actor = void 0;
|
|
3
|
+
exports.CallType = exports.Mode = exports.Actor = void 0;
|
|
4
4
|
var Actor;
|
|
5
5
|
(function (Actor) {
|
|
6
6
|
Actor["ACTOR"] = "actor";
|
|
@@ -12,3 +12,9 @@ var Mode;
|
|
|
12
12
|
Mode["FAIL"] = "fail";
|
|
13
13
|
Mode["CATCH"] = "catch";
|
|
14
14
|
})(Mode || (exports.Mode = Mode = {}));
|
|
15
|
+
var CallType;
|
|
16
|
+
(function (CallType) {
|
|
17
|
+
CallType["Internal"] = "internal";
|
|
18
|
+
CallType["HighLevel"] = "high-level";
|
|
19
|
+
CallType["LowLevel"] = "low-level";
|
|
20
|
+
})(CallType || (exports.CallType = CallType = {}));
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration } from 'solc-typed-ast';
|
|
2
|
+
import { CallType } from './types';
|
|
1
3
|
export declare function fileExists(p: string): Promise<boolean>;
|
|
2
4
|
export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
|
|
3
5
|
export declare function findOutputDirectory(workspaceRoot: string, foundryConfigPath: string): Promise<string>;
|
|
@@ -5,3 +7,25 @@ export declare function findSrcDirectory(workspaceRoot: string, foundryConfigPat
|
|
|
5
7
|
export declare function getTestFolder(foundryRoot: string): Promise<string>;
|
|
6
8
|
export declare function stripAnsiCodes(text: string): string;
|
|
7
9
|
export declare function getEnvPath(): string;
|
|
10
|
+
export declare const getLines: (content: string, src: string) => {
|
|
11
|
+
start: any;
|
|
12
|
+
end: any;
|
|
13
|
+
};
|
|
14
|
+
export declare const getSource: (content: string, src: string) => string;
|
|
15
|
+
export declare const getIndex: (content: string, index: number) => number;
|
|
16
|
+
export declare function highLevelCallWithOptions(fnCall: FunctionCall, noStatic?: boolean): boolean;
|
|
17
|
+
export declare function highLevelCall(fnCall: FunctionCall, noStatic?: boolean): boolean;
|
|
18
|
+
export declare function lowLevelCallWithOptions(fnCall: FunctionCall): boolean;
|
|
19
|
+
export declare function lowLevelCall(fnCall: FunctionCall): boolean;
|
|
20
|
+
export declare function lowLevelStaticCall(fnCall: FunctionCall): boolean;
|
|
21
|
+
export declare function lowLevelDelegateCall(fnCall: FunctionCall): boolean;
|
|
22
|
+
export declare function lowLevelSend(fnCall: FunctionCall): boolean;
|
|
23
|
+
export declare function lowLevelTransfer(fnCall: FunctionCall): boolean;
|
|
24
|
+
export declare function isStateVarAssignment(node: Assignment): boolean;
|
|
25
|
+
export declare function getStateVarAssignment(node: Assignment): VariableDeclaration | null;
|
|
26
|
+
export declare function getDeepRef(node: ASTNode): ASTNode | undefined;
|
|
27
|
+
export declare function getDefinitions(contract: ContractDefinition, kind: string, inclusion?: boolean): ASTNode[];
|
|
28
|
+
export declare function toSource(node: ASTNode, version?: string): string;
|
|
29
|
+
export declare function getCallType(fnCall: FunctionCall): CallType;
|
|
30
|
+
export declare const getFunctionName: (fnDef: FunctionDefinition) => string;
|
|
31
|
+
export declare const signatureEquals: (a: FunctionDefinition, b: FunctionDefinition) => boolean;
|
package/dist/utils.js
CHANGED
|
@@ -32,7 +32,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.signatureEquals = exports.getFunctionName = exports.getIndex = exports.getSource = exports.getLines = void 0;
|
|
36
40
|
exports.fileExists = fileExists;
|
|
37
41
|
exports.getFoundryConfigPath = getFoundryConfigPath;
|
|
38
42
|
exports.findOutputDirectory = findOutputDirectory;
|
|
@@ -40,8 +44,25 @@ exports.findSrcDirectory = findSrcDirectory;
|
|
|
40
44
|
exports.getTestFolder = getTestFolder;
|
|
41
45
|
exports.stripAnsiCodes = stripAnsiCodes;
|
|
42
46
|
exports.getEnvPath = getEnvPath;
|
|
47
|
+
exports.highLevelCallWithOptions = highLevelCallWithOptions;
|
|
48
|
+
exports.highLevelCall = highLevelCall;
|
|
49
|
+
exports.lowLevelCallWithOptions = lowLevelCallWithOptions;
|
|
50
|
+
exports.lowLevelCall = lowLevelCall;
|
|
51
|
+
exports.lowLevelStaticCall = lowLevelStaticCall;
|
|
52
|
+
exports.lowLevelDelegateCall = lowLevelDelegateCall;
|
|
53
|
+
exports.lowLevelSend = lowLevelSend;
|
|
54
|
+
exports.lowLevelTransfer = lowLevelTransfer;
|
|
55
|
+
exports.isStateVarAssignment = isStateVarAssignment;
|
|
56
|
+
exports.getStateVarAssignment = getStateVarAssignment;
|
|
57
|
+
exports.getDeepRef = getDeepRef;
|
|
58
|
+
exports.getDefinitions = getDefinitions;
|
|
59
|
+
exports.toSource = toSource;
|
|
60
|
+
exports.getCallType = getCallType;
|
|
43
61
|
const fs = __importStar(require("fs/promises"));
|
|
44
62
|
const path = __importStar(require("path"));
|
|
63
|
+
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
64
|
+
const src_location_1 = __importDefault(require("src-location"));
|
|
65
|
+
const types_1 = require("./types");
|
|
45
66
|
async function fileExists(p) {
|
|
46
67
|
try {
|
|
47
68
|
await fs.access(p);
|
|
@@ -104,3 +125,227 @@ function stripAnsiCodes(text) {
|
|
|
104
125
|
function getEnvPath() {
|
|
105
126
|
return process.env.PATH || '';
|
|
106
127
|
}
|
|
128
|
+
const getLines = (content, src) => {
|
|
129
|
+
let en = (0, exports.getIndex)(content, parseInt(src.split(':')[0]) + parseInt(src.split(':')[1]));
|
|
130
|
+
let { line: start } = src_location_1.default.indexToLocation(content, (0, exports.getIndex)(content, parseInt(src.split(':')[0])), true);
|
|
131
|
+
let { line: end } = src_location_1.default.indexToLocation(content, en, true);
|
|
132
|
+
return { start, end };
|
|
133
|
+
};
|
|
134
|
+
exports.getLines = getLines;
|
|
135
|
+
const getSource = (content, src) => {
|
|
136
|
+
let en = (0, exports.getIndex)(content, parseInt(src.split(':')[0]) + parseInt(src.split(':')[1]));
|
|
137
|
+
return content.slice((0, exports.getIndex)(content, parseInt(src.split(':')[0])), en);
|
|
138
|
+
};
|
|
139
|
+
exports.getSource = getSource;
|
|
140
|
+
const getIndex = (content, index) => {
|
|
141
|
+
const sourceBytes = (new TextEncoder).encode(content);
|
|
142
|
+
return new TextDecoder().decode(sourceBytes.slice(0, index)).length;
|
|
143
|
+
};
|
|
144
|
+
exports.getIndex = getIndex;
|
|
145
|
+
function highLevelCallWithOptions(fnCall, noStatic = false) {
|
|
146
|
+
var _a, _b;
|
|
147
|
+
if (!(fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess) ||
|
|
148
|
+
!(fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess))
|
|
149
|
+
return false;
|
|
150
|
+
const ref = (_a = fnCall.vExpression) === null || _a === void 0 ? void 0 : _a.vExpression.vReferencedDeclaration;
|
|
151
|
+
if (!(ref instanceof solc_typed_ast_1.FunctionDefinition))
|
|
152
|
+
return false;
|
|
153
|
+
if (noStatic) {
|
|
154
|
+
if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure ||
|
|
155
|
+
ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
|
|
156
|
+
ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Constant) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (fnCall.vExpression.vExpression.vExpression.typeString.startsWith('type(library ')) {
|
|
161
|
+
if (!ref.vReturnParameters || ((_b = ref.vReturnParameters) === null || _b === void 0 ? void 0 : _b.vParameters.length) === 0)
|
|
162
|
+
return false;
|
|
163
|
+
for (const inFnCall of ref.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
|
|
164
|
+
if (highLevelCall(inFnCall, noStatic))
|
|
165
|
+
return true;
|
|
166
|
+
if (highLevelCallWithOptions(inFnCall, noStatic))
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return (fnCall.vExpression.vExpression.vExpression.typeString.startsWith('contract ') ||
|
|
171
|
+
fnCall.vExpression.vExpression.typeString === 'address');
|
|
172
|
+
}
|
|
173
|
+
function highLevelCall(fnCall, noStatic = false) {
|
|
174
|
+
var _a, _b, _c;
|
|
175
|
+
if (!(fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess))
|
|
176
|
+
return false;
|
|
177
|
+
const ref = fnCall.vExpression.vReferencedDeclaration;
|
|
178
|
+
if (!(ref instanceof solc_typed_ast_1.FunctionDefinition))
|
|
179
|
+
return false;
|
|
180
|
+
if (noStatic) {
|
|
181
|
+
if (ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Pure ||
|
|
182
|
+
ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.View ||
|
|
183
|
+
ref.stateMutability === solc_typed_ast_1.FunctionStateMutability.Constant) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (fnCall.vExpression.vExpression.typeString.startsWith('type(library ')) {
|
|
188
|
+
if (!ref.vReturnParameters || ((_a = ref.vReturnParameters) === null || _a === void 0 ? void 0 : _a.vParameters.length) === 0)
|
|
189
|
+
return false;
|
|
190
|
+
for (const inFnCall of ref.getChildrenByType(solc_typed_ast_1.FunctionCall)) {
|
|
191
|
+
if (highLevelCall(inFnCall, noStatic))
|
|
192
|
+
return true;
|
|
193
|
+
if (highLevelCallWithOptions(inFnCall, noStatic))
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return (fnCall.vExpression.vExpression.typeString.startsWith('contract ') ||
|
|
198
|
+
((_c = (_b = fnCall.vExpression) === null || _b === void 0 ? void 0 : _b.vExpression) === null || _c === void 0 ? void 0 : _c.typeString) === 'address');
|
|
199
|
+
}
|
|
200
|
+
function lowLevelCallWithOptions(fnCall) {
|
|
201
|
+
return (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
202
|
+
fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
203
|
+
fnCall.vExpression.vExpression.memberName === 'call');
|
|
204
|
+
}
|
|
205
|
+
function lowLevelCall(fnCall) {
|
|
206
|
+
if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess && fnCall.vExpression.memberName === 'call') {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
else if (lowLevelCallWithOptions(fnCall)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
function lowLevelStaticCall(fnCall) {
|
|
215
|
+
if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
216
|
+
fnCall.vExpression.memberName === 'staticcall') {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
|
|
220
|
+
fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
221
|
+
fnCall.vExpression.vExpression.memberName === 'staticcall') {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
function lowLevelDelegateCall(fnCall) {
|
|
227
|
+
if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
228
|
+
fnCall.vExpression.memberName === 'delegatecall') {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
|
|
232
|
+
fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
233
|
+
fnCall.vExpression.vExpression.memberName === 'delegatecall') {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function lowLevelSend(fnCall) {
|
|
239
|
+
if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess && fnCall.vExpression.memberName === 'send') {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
|
|
243
|
+
fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
244
|
+
fnCall.vExpression.vExpression.memberName === 'send') {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
function lowLevelTransfer(fnCall) {
|
|
250
|
+
if (fnCall.vExpression instanceof solc_typed_ast_1.MemberAccess && fnCall.vExpression.memberName === 'transfer') {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
else if (fnCall.vExpression instanceof solc_typed_ast_1.FunctionCall &&
|
|
254
|
+
fnCall.vExpression.vExpression instanceof solc_typed_ast_1.MemberAccess &&
|
|
255
|
+
fnCall.vExpression.vExpression.memberName === 'transfer') {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
function isStateVarAssignment(node) {
|
|
261
|
+
const decl = getStateVarAssignment(node);
|
|
262
|
+
if (!decl)
|
|
263
|
+
return false;
|
|
264
|
+
return decl && (decl.stateVariable || decl.storageLocation === solc_typed_ast_1.DataLocation.Storage);
|
|
265
|
+
}
|
|
266
|
+
function getStateVarAssignment(node) {
|
|
267
|
+
const decl = getDeepRef(node.vLeftHandSide);
|
|
268
|
+
if (!(decl instanceof solc_typed_ast_1.VariableDeclaration))
|
|
269
|
+
return null;
|
|
270
|
+
return decl;
|
|
271
|
+
}
|
|
272
|
+
function getDeepRef(node) {
|
|
273
|
+
if (node instanceof solc_typed_ast_1.Identifier) {
|
|
274
|
+
return node.vReferencedDeclaration;
|
|
275
|
+
}
|
|
276
|
+
else if (node instanceof solc_typed_ast_1.IndexAccess) {
|
|
277
|
+
return getDeepRef(node.vBaseExpression);
|
|
278
|
+
}
|
|
279
|
+
else if (node instanceof solc_typed_ast_1.MemberAccess) {
|
|
280
|
+
return getDeepRef(node.vExpression);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function getDefinitions(contract, kind, inclusion = true) {
|
|
287
|
+
const visited = new Set();
|
|
288
|
+
const gather = (c) => {
|
|
289
|
+
if (visited.has(c)) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
visited.add(c);
|
|
293
|
+
const own = inclusion ? c[kind] : [];
|
|
294
|
+
const defs = [...(own || [])];
|
|
295
|
+
for (const base of c.vLinearizedBaseContracts) {
|
|
296
|
+
if (base === c) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
for (const def of gather(base)) {
|
|
300
|
+
if (!defs.includes(def)) {
|
|
301
|
+
defs.push(def);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return defs;
|
|
306
|
+
};
|
|
307
|
+
return gather(contract);
|
|
308
|
+
}
|
|
309
|
+
function toSource(node, version) {
|
|
310
|
+
const formatter = new solc_typed_ast_1.PrettyFormatter(4, 0);
|
|
311
|
+
const writer = new solc_typed_ast_1.ASTWriter(solc_typed_ast_1.DefaultASTWriterMapping, formatter, version ? version : solc_typed_ast_1.LatestCompilerVersion);
|
|
312
|
+
return writer.write(node);
|
|
313
|
+
}
|
|
314
|
+
function getCallType(fnCall) {
|
|
315
|
+
if (highLevelCall(fnCall)) {
|
|
316
|
+
return types_1.CallType.HighLevel;
|
|
317
|
+
}
|
|
318
|
+
else if (lowLevelCall(fnCall)) {
|
|
319
|
+
return types_1.CallType.LowLevel;
|
|
320
|
+
}
|
|
321
|
+
return types_1.CallType.Internal;
|
|
322
|
+
}
|
|
323
|
+
const getFunctionName = (fnDef) => {
|
|
324
|
+
if (fnDef.name) {
|
|
325
|
+
return fnDef.name;
|
|
326
|
+
}
|
|
327
|
+
if (fnDef.isConstructor || fnDef.kind === solc_typed_ast_1.FunctionKind.Constructor) {
|
|
328
|
+
return 'constructor';
|
|
329
|
+
}
|
|
330
|
+
if (fnDef.kind === solc_typed_ast_1.FunctionKind.Fallback) {
|
|
331
|
+
return 'fallback';
|
|
332
|
+
}
|
|
333
|
+
if (fnDef.kind === solc_typed_ast_1.FunctionKind.Receive) {
|
|
334
|
+
return 'receive';
|
|
335
|
+
}
|
|
336
|
+
return 'Unknown';
|
|
337
|
+
};
|
|
338
|
+
exports.getFunctionName = getFunctionName;
|
|
339
|
+
const signatureEquals = (a, b) => {
|
|
340
|
+
if (!a.name || !b.name)
|
|
341
|
+
return false;
|
|
342
|
+
if (a.name !== b.name)
|
|
343
|
+
return false;
|
|
344
|
+
if (a.vParameters.vParameters.map((x) => x.type).join(',') !==
|
|
345
|
+
b.vParameters.vParameters.map((x) => x.type).join(','))
|
|
346
|
+
return false;
|
|
347
|
+
if (a.visibility !== b.visibility)
|
|
348
|
+
return false;
|
|
349
|
+
return a.stateMutability === b.stateMutability;
|
|
350
|
+
};
|
|
351
|
+
exports.signatureEquals = signatureEquals;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recon-generate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"case": "^1.6.3",
|
|
24
24
|
"commander": "^14.0.2",
|
|
25
25
|
"handlebars": "^4.7.8",
|
|
26
|
+
"solc-typed-ast": "^19.1.0",
|
|
27
|
+
"src-location": "^1.1.0",
|
|
26
28
|
"yaml": "^2.8.2"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|