recon-generate 0.0.10 → 0.0.11
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/generator.d.ts +2 -0
- package/dist/generator.js +71 -0
- package/dist/index.js +55 -1
- package/dist/pathsGenerator.d.ts +5 -1
- package/dist/pathsGenerator.js +62 -11
- package/dist/templates/setup.js +4 -3
- package/dist/templates/wake/before_after.d.ts +1 -0
- package/dist/templates/wake/before_after.js +33 -0
- package/dist/templates/wake/conftest.d.ts +1 -0
- package/dist/templates/wake/conftest.js +18 -0
- package/dist/templates/wake/crytic_tester.d.ts +1 -0
- package/dist/templates/wake/crytic_tester.js +20 -0
- package/dist/templates/wake/flows.d.ts +1 -0
- package/dist/templates/wake/flows.js +18 -0
- package/dist/templates/wake/fuzz-test.d.ts +1 -0
- package/dist/templates/wake/fuzz-test.js +62 -0
- package/dist/templates/wake/handlebars_helpers.d.ts +2 -0
- package/dist/templates/wake/handlebars_helpers.js +84 -0
- package/dist/templates/wake/helpers/actor_manager.d.ts +1 -0
- package/dist/templates/wake/helpers/actor_manager.js +44 -0
- package/dist/templates/wake/helpers/asset_manager.d.ts +1 -0
- package/dist/templates/wake/helpers/asset_manager.js +44 -0
- package/dist/templates/wake/helpers/utils.d.ts +1 -0
- package/dist/templates/wake/helpers/utils.js +11 -0
- package/dist/templates/wake/helpers.d.ts +2 -0
- package/dist/templates/wake/helpers.js +81 -0
- package/dist/templates/wake/index.d.ts +10 -0
- package/dist/templates/wake/index.js +26 -0
- package/dist/templates/wake/invariants.d.ts +1 -0
- package/dist/templates/wake/invariants.js +21 -0
- package/dist/templates/wake/properties.d.ts +1 -0
- package/dist/templates/wake/properties.js +19 -0
- package/dist/templates/wake/setup.d.ts +1 -0
- package/dist/templates/wake/setup.js +48 -0
- package/dist/templates/wake/target_functions.d.ts +1 -0
- package/dist/templates/wake/target_functions.js +24 -0
- package/dist/templates/wake/targets/contract_targets.d.ts +1 -0
- package/dist/templates/wake/targets/contract_targets.js +23 -0
- package/dist/templates/wake/targets/managers_targets.d.ts +1 -0
- package/dist/templates/wake/targets/managers_targets.js +26 -0
- package/dist/templates/wake/test_fuzz.d.ts +1 -0
- package/dist/templates/wake/test_fuzz.js +27 -0
- package/dist/wakeGenerator.d.ts +32 -0
- package/dist/wakeGenerator.js +447 -0
- package/package.json +1 -1
package/dist/generator.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare class ReconGenerator {
|
|
|
24
24
|
private generatedMocks;
|
|
25
25
|
private allowedMockNames;
|
|
26
26
|
private contractKindCache;
|
|
27
|
+
private abstractContractCache;
|
|
27
28
|
constructor(foundryRoot: string, options: GeneratorOptions);
|
|
28
29
|
private logDebug;
|
|
29
30
|
private outDir;
|
|
@@ -37,6 +38,7 @@ export declare class ReconGenerator {
|
|
|
37
38
|
private updateRemappings;
|
|
38
39
|
private updateGitignore;
|
|
39
40
|
private getContractKind;
|
|
41
|
+
private isAbstractContract;
|
|
40
42
|
private findSourceContracts;
|
|
41
43
|
private matchesSignatureOrName;
|
|
42
44
|
private paramType;
|
package/dist/generator.js
CHANGED
|
@@ -54,6 +54,7 @@ class ReconGenerator {
|
|
|
54
54
|
this.generatedMocks = new Set();
|
|
55
55
|
this.allowedMockNames = new Set();
|
|
56
56
|
this.contractKindCache = new Map();
|
|
57
|
+
this.abstractContractCache = new Map();
|
|
57
58
|
}
|
|
58
59
|
logDebug(message, obj) {
|
|
59
60
|
if (!this.options.debug)
|
|
@@ -196,6 +197,53 @@ class ReconGenerator {
|
|
|
196
197
|
this.contractKindCache.set(absPath, fileCache);
|
|
197
198
|
return kind;
|
|
198
199
|
}
|
|
200
|
+
async isAbstractContract(sourcePath, contractName) {
|
|
201
|
+
const absPath = path.isAbsolute(sourcePath) ? sourcePath : path.join(this.foundryRoot, sourcePath);
|
|
202
|
+
let fileCache = this.abstractContractCache.get(absPath);
|
|
203
|
+
if (fileCache && fileCache.has(contractName)) {
|
|
204
|
+
return fileCache.get(contractName);
|
|
205
|
+
}
|
|
206
|
+
let content;
|
|
207
|
+
try {
|
|
208
|
+
content = await fs.readFile(absPath, 'utf8');
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
this.logDebug('Failed to read source when determining abstract contract', { sourcePath, error: String(e) });
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
let ast;
|
|
215
|
+
try {
|
|
216
|
+
ast = parser_1.default.parse(content, { tolerant: true });
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
this.logDebug('Failed to parse source when determining abstract contract', { sourcePath, error: String(e) });
|
|
220
|
+
ast = null;
|
|
221
|
+
}
|
|
222
|
+
fileCache = fileCache !== null && fileCache !== void 0 ? fileCache : new Map();
|
|
223
|
+
let isAbstract = false;
|
|
224
|
+
const children = ast === null || ast === void 0 ? void 0 : ast.children;
|
|
225
|
+
if (Array.isArray(children)) {
|
|
226
|
+
for (const node of children) {
|
|
227
|
+
if ((node === null || node === void 0 ? void 0 : node.type) === 'ContractDefinition' && node.name === contractName) {
|
|
228
|
+
if (node.abstract === true) {
|
|
229
|
+
isAbstract = true;
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!isAbstract) {
|
|
236
|
+
// Fallback: text search for `abstract contract <Name>` to cover parser misses
|
|
237
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
238
|
+
const pattern = new RegExp(`\\babstract\\s+contract\\s+${contractName}\\b`);
|
|
239
|
+
if (pattern.test(stripped)) {
|
|
240
|
+
isAbstract = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
fileCache.set(contractName, isAbstract);
|
|
244
|
+
this.abstractContractCache.set(absPath, fileCache);
|
|
245
|
+
return isAbstract;
|
|
246
|
+
}
|
|
199
247
|
async findSourceContracts() {
|
|
200
248
|
var _a, _b;
|
|
201
249
|
const contracts = [];
|
|
@@ -226,11 +274,34 @@ class ReconGenerator {
|
|
|
226
274
|
const lowerName = String(contractName).toLowerCase();
|
|
227
275
|
const isGeneratedMock = this.generatedMocks.has(String(contractName));
|
|
228
276
|
const contractKind = await this.getContractKind(sourcePath, String(contractName));
|
|
277
|
+
const isAbstract = await this.isAbstractContract(sourcePath, String(contractName));
|
|
278
|
+
const absSourcePath = path.isAbsolute(sourcePath)
|
|
279
|
+
? sourcePath
|
|
280
|
+
: path.join(this.foundryRoot, sourcePath);
|
|
281
|
+
const relSourcePath = path.relative(this.foundryRoot, absSourcePath).replace(/\\/g, '/').toLowerCase();
|
|
282
|
+
const skipPrefixes = [
|
|
283
|
+
'test/',
|
|
284
|
+
'tests/',
|
|
285
|
+
'script/',
|
|
286
|
+
'scripts/',
|
|
287
|
+
'contracts/test/',
|
|
288
|
+
'contracts/tests/',
|
|
289
|
+
'lib/',
|
|
290
|
+
];
|
|
229
291
|
// Check if contract is explicitly included or mocked - if so, don't filter by kind
|
|
230
292
|
const isExplicitlyIncluded = this.options.include &&
|
|
231
293
|
(this.options.include.contractOnly.has(String(contractName)) ||
|
|
232
294
|
this.options.include.functions.has(String(contractName)));
|
|
233
295
|
const isExplicitlyMocked = (_b = this.options.mocks) === null || _b === void 0 ? void 0 : _b.has(String(contractName));
|
|
296
|
+
const inSkipDir = skipPrefixes.some(prefix => relSourcePath.startsWith(prefix));
|
|
297
|
+
if (!isGeneratedMock && inSkipDir && !isExplicitlyIncluded && !isExplicitlyMocked) {
|
|
298
|
+
this.logDebug('Skipping artifact from non-source directory', { contractName, sourcePath });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (isAbstract && !isExplicitlyIncluded && !isExplicitlyMocked) {
|
|
302
|
+
this.logDebug('Skipping abstract contract', { contractName, sourcePath });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
234
305
|
if ((contractKind === 'interface' || contractKind === 'library') && !isExplicitlyIncluded && !isExplicitlyMocked) {
|
|
235
306
|
this.logDebug('Skipping non-contract artifact', { contractName, sourcePath, contractKind });
|
|
236
307
|
continue;
|
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 wakeGenerator_1 = require("./wakeGenerator");
|
|
41
42
|
const coverage_1 = require("./coverage");
|
|
42
43
|
const pathsGenerator_1 = require("./pathsGenerator");
|
|
43
44
|
const utils_1 = require("./utils");
|
|
@@ -146,6 +147,8 @@ async function main() {
|
|
|
146
147
|
.option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
|
|
147
148
|
.option('--name <suite>', 'Suite name; affects paths filename (recon-<name>-paths.json)')
|
|
148
149
|
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
150
|
+
.option('--dry-run', 'Do not enumerate paths; only report which functions/external calls would be processed')
|
|
151
|
+
.option('--max-paths <n>', 'Cap path enumeration per function to avoid OOM (default: 5000)')
|
|
149
152
|
.action(async (opts, cmd) => {
|
|
150
153
|
var _a;
|
|
151
154
|
const workspaceRoot = process.cwd();
|
|
@@ -154,7 +157,11 @@ async function main() {
|
|
|
154
157
|
const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
|
|
155
158
|
const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
|
|
156
159
|
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
157
|
-
|
|
160
|
+
const maxPaths = opts.maxPaths ? parseInt(String(opts.maxPaths), 10) : 5000;
|
|
161
|
+
await (0, pathsGenerator_1.runPaths)(foundryRoot, opts.cryticName || 'CryticTester', suiteSnake, {
|
|
162
|
+
dryRun: !!opts.dryRun,
|
|
163
|
+
maxPaths: Number.isFinite(maxPaths) && maxPaths > 0 ? maxPaths : undefined,
|
|
164
|
+
});
|
|
158
165
|
});
|
|
159
166
|
program
|
|
160
167
|
.command('link')
|
|
@@ -185,6 +192,53 @@ async function main() {
|
|
|
185
192
|
: path.join(foundryRoot, medusaConfigOpt);
|
|
186
193
|
await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath, !!opts.verbose);
|
|
187
194
|
});
|
|
195
|
+
program
|
|
196
|
+
.command('wake')
|
|
197
|
+
.description('Generate Python fuzzing suite using Wake framework')
|
|
198
|
+
.option('--include <spec>', 'Include filter: e.g. "A,B:{foo(uint256)|bar(address)},C"')
|
|
199
|
+
.option('--exclude <spec>', 'Exclude filter: e.g. "A:{foo(uint256)},D" or just "B"')
|
|
200
|
+
.option('--admin <spec>', 'Mark functions as admin: e.g. "A:{foo(uint256),bar()},B:{baz(address)}"')
|
|
201
|
+
.option('--name <suite>', 'Suite name; affects folder (tests/recon-name)')
|
|
202
|
+
.option('-o, --output <path>', 'Custom output folder for tests')
|
|
203
|
+
.option('--debug', 'Print debug info about filters and selection')
|
|
204
|
+
.option('--list', 'List available contracts/functions (after filters) and exit')
|
|
205
|
+
.option('--force', 'Replace existing generated suite output')
|
|
206
|
+
.option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
|
|
207
|
+
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
208
|
+
.action(async (opts, cmd) => {
|
|
209
|
+
var _a;
|
|
210
|
+
const workspaceRoot = process.cwd();
|
|
211
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
212
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
213
|
+
const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
|
|
214
|
+
const includeFilter = parseFilter(opts.include);
|
|
215
|
+
const excludeFilter = parseFilter(opts.exclude);
|
|
216
|
+
const adminFilter = parseFilter(opts.admin);
|
|
217
|
+
const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
|
|
218
|
+
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
219
|
+
const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
|
|
220
|
+
const suiteFolderName = suiteSnake ? `recon_${suiteSnake}` : 'recon';
|
|
221
|
+
const baseOut = opts.output ? String(opts.output) : 'tests';
|
|
222
|
+
const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.resolve(foundryRoot, baseOut);
|
|
223
|
+
const suiteDir = path.resolve(baseOutPath, suiteFolderName);
|
|
224
|
+
const generator = new wakeGenerator_1.WakeGenerator(foundryRoot, {
|
|
225
|
+
suiteDir,
|
|
226
|
+
foundryConfigPath: foundryConfig,
|
|
227
|
+
include: includeFilter,
|
|
228
|
+
exclude: excludeFilter,
|
|
229
|
+
admin: adminFilter ? adminFilter.functions : undefined,
|
|
230
|
+
debug: !!opts.debug || !!parentOpts.debug,
|
|
231
|
+
force: Boolean(opts.force || parentOpts.force),
|
|
232
|
+
forceBuild: !!opts.forceBuild || !!parentOpts.forceBuild,
|
|
233
|
+
suiteNameSnake: suiteSnake,
|
|
234
|
+
suiteNamePascal: suitePascal,
|
|
235
|
+
});
|
|
236
|
+
if (opts.list) {
|
|
237
|
+
await generator.listAvailable();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await generator.run();
|
|
241
|
+
});
|
|
188
242
|
program
|
|
189
243
|
.action(async (opts) => {
|
|
190
244
|
const workspaceRoot = process.cwd();
|
package/dist/pathsGenerator.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Generates minimal paths needed for 100% branch coverage.
|
|
5
5
|
* Output is optimized for LLM consumption.
|
|
6
6
|
*/
|
|
7
|
+
export interface RunPathsOptions {
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
maxPaths?: number;
|
|
10
|
+
}
|
|
7
11
|
/** Simple output: function name -> array of path condition strings */
|
|
8
12
|
export type PathOutput = Record<string, string[]>;
|
|
9
|
-
export declare const runPaths: (foundryRoot: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;
|
|
13
|
+
export declare const runPaths: (foundryRoot: string, cryticName: string, suiteNameSnake?: string, options?: RunPathsOptions) => Promise<void>;
|
package/dist/pathsGenerator.js
CHANGED
|
@@ -103,7 +103,7 @@ const loadLatestSourceUnits = async (foundryRoot) => {
|
|
|
103
103
|
return reader.read(filteredAstData);
|
|
104
104
|
};
|
|
105
105
|
// ==================== Main Entry ====================
|
|
106
|
-
const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
|
|
106
|
+
const runPaths = async (foundryRoot, cryticName, suiteNameSnake, options = {}) => {
|
|
107
107
|
const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
|
|
108
108
|
await runCmd(buildCmd, foundryRoot);
|
|
109
109
|
const sourceUnits = await loadLatestSourceUnits(foundryRoot);
|
|
@@ -123,6 +123,18 @@ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
|
|
|
123
123
|
// Process each function
|
|
124
124
|
const output = {};
|
|
125
125
|
let totalPaths = 0;
|
|
126
|
+
if (options.dryRun) {
|
|
127
|
+
console.log(`[dry-run] Crytic contract: ${cryticName}`);
|
|
128
|
+
console.log(`[dry-run] Target functions (${targetFunctions.length}):`);
|
|
129
|
+
for (const func of targetFunctions) {
|
|
130
|
+
const externalCall = findExternalCall(func);
|
|
131
|
+
const extLabel = externalCall ? `${externalCall.contract}.${externalCall.function}` : 'none';
|
|
132
|
+
console.log(` - ${func.name} -> external call: ${extLabel}`);
|
|
133
|
+
}
|
|
134
|
+
console.log('[dry-run] Skipping path enumeration.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const maxPaths = options.maxPaths && options.maxPaths > 0 ? options.maxPaths : undefined;
|
|
126
138
|
for (const func of targetFunctions) {
|
|
127
139
|
// Find external call
|
|
128
140
|
const externalCall = findExternalCall(func);
|
|
@@ -145,8 +157,8 @@ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
|
|
|
145
157
|
const callTree = (0, call_tree_builder_1.buildCallTree)(extFunc, [], undefined, undefined, context);
|
|
146
158
|
// Build param mapping
|
|
147
159
|
const paramMapping = buildParamMapping(func, externalCall);
|
|
148
|
-
// Enumerate paths
|
|
149
|
-
const enumerator = new PathEnumerator(sourceUnits, paramMapping);
|
|
160
|
+
// Enumerate paths with optional cap
|
|
161
|
+
const enumerator = new PathEnumerator(sourceUnits, paramMapping, maxPaths);
|
|
150
162
|
const paths = enumerator.enumerate(callTree);
|
|
151
163
|
if (paths.length === 0)
|
|
152
164
|
continue;
|
|
@@ -157,11 +169,14 @@ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
|
|
|
157
169
|
continue;
|
|
158
170
|
output[func.name] = simplified;
|
|
159
171
|
totalPaths += simplified.length;
|
|
172
|
+
if (enumerator.wasTruncated()) {
|
|
173
|
+
console.warn(`[recon-generate] Warning: Path enumeration for ${func.name} reached the max limit${maxPaths ? ` (${maxPaths})` : ''} and was truncated.`);
|
|
174
|
+
}
|
|
160
175
|
}
|
|
161
176
|
const pathsName = suiteNameSnake ? `recon-${suiteNameSnake}-paths.json` : 'recon-paths.json';
|
|
162
177
|
const pathsFilePath = path.join(foundryRoot, pathsName);
|
|
163
178
|
await fs.writeFile(pathsFilePath, JSON.stringify(output, null, 2));
|
|
164
|
-
console.log(`[recon-generate] Wrote paths file to ${pathsFilePath}`);
|
|
179
|
+
console.log(`[recon-generate] Wrote paths file to ${pathsFilePath} (total paths: ${totalPaths})`);
|
|
165
180
|
};
|
|
166
181
|
exports.runPaths = runPaths;
|
|
167
182
|
// ==================== Source Helpers ====================
|
|
@@ -224,20 +239,26 @@ function buildParamMapping(func, extCall) {
|
|
|
224
239
|
}
|
|
225
240
|
// ==================== Path Enumeration ====================
|
|
226
241
|
class PathEnumerator {
|
|
227
|
-
constructor(sourceUnits, paramMap) {
|
|
242
|
+
constructor(sourceUnits, paramMap, maxPaths) {
|
|
228
243
|
this.pathIdCounter = 0;
|
|
229
244
|
this.branchPoints = 0;
|
|
245
|
+
this.truncated = false;
|
|
230
246
|
this.sourceUnits = sourceUnits;
|
|
231
247
|
this.paramMap = new Map(paramMap);
|
|
248
|
+
this.maxPaths = maxPaths;
|
|
232
249
|
}
|
|
233
250
|
enumerate(callTree) {
|
|
234
251
|
this.pathIdCounter = 0;
|
|
235
252
|
this.branchPoints = 0;
|
|
253
|
+
this.truncated = false;
|
|
236
254
|
const paths = this.processCallTree(callTree);
|
|
237
255
|
return paths
|
|
238
256
|
.filter(p => p.result === 'success')
|
|
239
257
|
.map(p => ({ id: p.id, conditions: p.conditions, requires: p.requires, result: p.result }));
|
|
240
258
|
}
|
|
259
|
+
wasTruncated() {
|
|
260
|
+
return this.truncated;
|
|
261
|
+
}
|
|
241
262
|
processCallTree(node) {
|
|
242
263
|
const def = node.definition;
|
|
243
264
|
// Process children first (leaf to root)
|
|
@@ -252,8 +273,13 @@ class PathEnumerator {
|
|
|
252
273
|
this.paramMap = savedMap;
|
|
253
274
|
}
|
|
254
275
|
// Process function body
|
|
276
|
+
const firstId = ++this.pathIdCounter;
|
|
277
|
+
if (this.maxPaths && firstId > this.maxPaths) {
|
|
278
|
+
this.truncated = true;
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
255
281
|
let activePaths = [{
|
|
256
|
-
id:
|
|
282
|
+
id: firstId,
|
|
257
283
|
conditions: [],
|
|
258
284
|
requires: [],
|
|
259
285
|
terminated: false
|
|
@@ -336,8 +362,13 @@ class PathEnumerator {
|
|
|
336
362
|
for (const clause of stmt.vClauses) {
|
|
337
363
|
if (clause.errorName === '') {
|
|
338
364
|
// Success clause (try block) - external call succeeds
|
|
365
|
+
const nextId = ++this.pathIdCounter;
|
|
366
|
+
if (this.maxPaths && nextId > this.maxPaths) {
|
|
367
|
+
this.truncated = true;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
339
370
|
const successPath = {
|
|
340
|
-
id:
|
|
371
|
+
id: nextId,
|
|
341
372
|
conditions: [...path.conditions, {
|
|
342
373
|
original: callExpr,
|
|
343
374
|
resolved: callExpr,
|
|
@@ -351,8 +382,13 @@ class PathEnumerator {
|
|
|
351
382
|
}
|
|
352
383
|
else {
|
|
353
384
|
// Catch clause - external call fails
|
|
385
|
+
const nextCatchId = ++this.pathIdCounter;
|
|
386
|
+
if (this.maxPaths && nextCatchId > this.maxPaths) {
|
|
387
|
+
this.truncated = true;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
354
390
|
const catchPath = {
|
|
355
|
-
id:
|
|
391
|
+
id: nextCatchId,
|
|
356
392
|
conditions: [...path.conditions, {
|
|
357
393
|
original: callExpr,
|
|
358
394
|
resolved: callExpr,
|
|
@@ -411,8 +447,13 @@ class PathEnumerator {
|
|
|
411
447
|
continue;
|
|
412
448
|
}
|
|
413
449
|
// True branch
|
|
450
|
+
const nextIdTrue = ++this.pathIdCounter;
|
|
451
|
+
if (this.maxPaths && nextIdTrue > this.maxPaths) {
|
|
452
|
+
this.truncated = true;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
414
455
|
const truePath = {
|
|
415
|
-
id:
|
|
456
|
+
id: nextIdTrue,
|
|
416
457
|
conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: true }],
|
|
417
458
|
requires: [...path.requires],
|
|
418
459
|
terminated: false
|
|
@@ -427,8 +468,13 @@ class PathEnumerator {
|
|
|
427
468
|
results.push(truePath);
|
|
428
469
|
}
|
|
429
470
|
// False branch
|
|
471
|
+
const nextIdFalse = ++this.pathIdCounter;
|
|
472
|
+
if (this.maxPaths && nextIdFalse > this.maxPaths) {
|
|
473
|
+
this.truncated = true;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
430
476
|
const falsePath = {
|
|
431
|
-
id:
|
|
477
|
+
id: nextIdFalse,
|
|
432
478
|
conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: false }],
|
|
433
479
|
requires: [...path.requires],
|
|
434
480
|
terminated: false
|
|
@@ -483,8 +529,13 @@ class PathEnumerator {
|
|
|
483
529
|
for (const cp of cPaths) {
|
|
484
530
|
if (cp.result === 'revert')
|
|
485
531
|
continue;
|
|
532
|
+
const nextId = ++this.pathIdCounter;
|
|
533
|
+
if (this.maxPaths && nextId > this.maxPaths) {
|
|
534
|
+
this.truncated = true;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
486
537
|
merged.push({
|
|
487
|
-
id:
|
|
538
|
+
id: nextId,
|
|
488
539
|
conditions: [...p.conditions, ...cp.conditions],
|
|
489
540
|
requires: [...p.requires, ...cp.requires],
|
|
490
541
|
terminated: false
|
package/dist/templates/setup.js
CHANGED
|
@@ -27,11 +27,12 @@ import "{{this.path}}";
|
|
|
27
27
|
{{/each}}
|
|
28
28
|
|
|
29
29
|
abstract contract Setup is BaseSetup, ActorManager, AssetManager, Utils {
|
|
30
|
-
|
|
30
|
+
{{#each contracts}}
|
|
31
31
|
{{this.name}} {{camel this.name}};
|
|
32
|
-
{{#if this.isDynamic}}
|
|
32
|
+
{{#if this.isDynamic}}
|
|
33
|
+
address[] {{camel this.name}}_s;
|
|
33
34
|
{{/if}}
|
|
34
|
-
|
|
35
|
+
{{/each}}
|
|
35
36
|
|
|
36
37
|
/// === Setup === ///
|
|
37
38
|
/// This contains all calls to be performed in the tester constructor, both for Echidna and Foundry
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const beforeAfterTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.beforeAfterTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
exports.beforeAfterTemplate = handlebars_1.default.compile(`
|
|
9
|
+
from wake.testing import *
|
|
10
|
+
from wake.testing.fuzzing import *
|
|
11
|
+
from .setup import Setup
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Vars:
|
|
16
|
+
# Example ghost variables for tracking state
|
|
17
|
+
# total_assets: int = 0
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class BeforeAfter(Setup):
|
|
21
|
+
_before: Vars
|
|
22
|
+
_after: Vars
|
|
23
|
+
|
|
24
|
+
def pre_flow(self, flow: Callable) -> None:
|
|
25
|
+
# Called before each flow
|
|
26
|
+
# self._before = Vars(total_assets=self.complexVault.totalAssets())
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def post_flow(self, flow: Callable) -> None:
|
|
30
|
+
# Called after each flow
|
|
31
|
+
# self._after = Vars(total_assets=self.complexVault.totalAssets())
|
|
32
|
+
pass
|
|
33
|
+
`, { noEscape: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const conftestTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.conftestTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
const handlebars_helpers_1 = require("./handlebars_helpers");
|
|
9
|
+
(0, handlebars_helpers_1.registerWakeHelpers)(handlebars_1.default);
|
|
10
|
+
exports.conftestTemplate = handlebars_1.default.compile(`import pytest
|
|
11
|
+
from wake.testing import chain
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
14
|
+
def chain_connection():
|
|
15
|
+
"""Connect to a local chain for all tests in this module."""
|
|
16
|
+
with chain.connect():
|
|
17
|
+
yield
|
|
18
|
+
`, { noEscape: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const cryticTesterTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cryticTesterTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
exports.cryticTesterTemplate = handlebars_1.default.compile(`
|
|
9
|
+
from wake.testing import *
|
|
10
|
+
from wake.testing.fuzzing import FuzzTest, flow, invariant
|
|
11
|
+
from .target_functions import TargetFunctions
|
|
12
|
+
|
|
13
|
+
class TestCrytic(TargetFunctions):
|
|
14
|
+
def pre_sequence(self):
|
|
15
|
+
self.setup()
|
|
16
|
+
|
|
17
|
+
@chain.connect()
|
|
18
|
+
def test_recon_fuzz():
|
|
19
|
+
TestCrytic().run(sequences_count=30, flows_count=100)
|
|
20
|
+
`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const flowsTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.flowsTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
const helpers_1 = require("./helpers");
|
|
9
|
+
(0, helpers_1.registerWakeHelpers)(handlebars_1.default);
|
|
10
|
+
exports.flowsTemplate = handlebars_1.default.compile(`
|
|
11
|
+
from wake.testing import *
|
|
12
|
+
from wake.testing.fuzzing import *
|
|
13
|
+
|
|
14
|
+
class Flows:
|
|
15
|
+
{{#each flows}}
|
|
16
|
+
{{flowDefinition this}}
|
|
17
|
+
{{/each}}
|
|
18
|
+
`, { noEscape: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const fuzzTestTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fuzzTestTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
const helpers_1 = require("./helpers");
|
|
9
|
+
(0, helpers_1.registerWakeHelpers)(handlebars_1.default);
|
|
10
|
+
exports.fuzzTestTemplate = handlebars_1.default.compile(`"""
|
|
11
|
+
{{pascal suiteName}} Fuzz Test
|
|
12
|
+
"""
|
|
13
|
+
from wake.testing import *
|
|
14
|
+
from wake.testing.fuzzing import *
|
|
15
|
+
import random
|
|
16
|
+
|
|
17
|
+
# Import pytypes
|
|
18
|
+
{{#each contracts}}
|
|
19
|
+
from {{module}} import {{name}}
|
|
20
|
+
{{/each}}
|
|
21
|
+
|
|
22
|
+
from .flows import Flows
|
|
23
|
+
from .invariants import Invariants
|
|
24
|
+
|
|
25
|
+
class {{pascal suiteName}}FuzzTest(FuzzTest, Flows, Invariants):
|
|
26
|
+
# Contract instances
|
|
27
|
+
{{#each contracts}}
|
|
28
|
+
{{camel name}}: {{name}}
|
|
29
|
+
{{/each}}
|
|
30
|
+
|
|
31
|
+
# Actors
|
|
32
|
+
actors: list[Account]
|
|
33
|
+
admin: Account
|
|
34
|
+
|
|
35
|
+
def pre_sequence(self) -> None:
|
|
36
|
+
# Setup actors
|
|
37
|
+
self.admin = default_chain.accounts[0]
|
|
38
|
+
self.actors = [Account.new() for _ in range(3)]
|
|
39
|
+
|
|
40
|
+
# Deploy contracts
|
|
41
|
+
{{#each contracts}}
|
|
42
|
+
# Deploy {{name}}
|
|
43
|
+
# Args: {{#each deploy_args}}{{name}}: {{type}}, {{/each}}
|
|
44
|
+
self.{{camel name}} = {{name}}.deploy(
|
|
45
|
+
{{#each deploy_args}}{{name}}={{pythonDefault type}}, {{/each}}
|
|
46
|
+
from_=self.admin
|
|
47
|
+
)
|
|
48
|
+
{{/each}}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_actor(self) -> Account:
|
|
52
|
+
return self.actors[0] if self.actors else self.admin
|
|
53
|
+
|
|
54
|
+
@flow()
|
|
55
|
+
def flow_switch_actor(self, entropy: uint8) -> None:
|
|
56
|
+
if self.actors:
|
|
57
|
+
idx = entropy % len(self.actors)
|
|
58
|
+
self.actors = self.actors[idx:] + self.actors[:idx]
|
|
59
|
+
|
|
60
|
+
def test_{{snake suiteName}}_fuzz():
|
|
61
|
+
{{pascal suiteName}}FuzzTest().run(sequences_count=10, flows_count=100)
|
|
62
|
+
`, { noEscape: true });
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerWakeHelpers = registerWakeHelpers;
|
|
4
|
+
const case_1 = require("case");
|
|
5
|
+
const types_1 = require("../../types"); // Keeping types for Actor/Mode
|
|
6
|
+
function registerWakeHelpers(handlebars) {
|
|
7
|
+
handlebars.registerHelper('snake', function (str) {
|
|
8
|
+
return (0, case_1.snake)(str);
|
|
9
|
+
});
|
|
10
|
+
handlebars.registerHelper('camel', function (str) {
|
|
11
|
+
return (0, case_1.camel)(str);
|
|
12
|
+
});
|
|
13
|
+
handlebars.registerHelper('pascal', function (str) {
|
|
14
|
+
return (0, case_1.pascal)(str);
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Python default value based on type string.
|
|
18
|
+
*/
|
|
19
|
+
function pythonDefault(type) {
|
|
20
|
+
if (type.includes("Address"))
|
|
21
|
+
return "Address(0)";
|
|
22
|
+
if (type.includes("int"))
|
|
23
|
+
return "0";
|
|
24
|
+
if (type === "bool")
|
|
25
|
+
return "False";
|
|
26
|
+
if (type.includes("bytes"))
|
|
27
|
+
return 'b""';
|
|
28
|
+
if (type.includes("list") || type.includes("List"))
|
|
29
|
+
return "[]";
|
|
30
|
+
return "()";
|
|
31
|
+
}
|
|
32
|
+
handlebars.registerHelper('pythonDefault', pythonDefault);
|
|
33
|
+
/**
|
|
34
|
+
* Generate a @flow decorated method definition for Wake FuzzTest.
|
|
35
|
+
* Expects: { contractName, method: { name, args: [{name, type}] }, actor, mode }
|
|
36
|
+
*/
|
|
37
|
+
handlebars.registerHelper('flowDefinition', function ({ contractName, method, actor, mode }) {
|
|
38
|
+
const contractVar = (0, case_1.camel)(contractName);
|
|
39
|
+
const funcName = method.name;
|
|
40
|
+
// Build parameter list
|
|
41
|
+
const params = method.args.map((arg) => {
|
|
42
|
+
let typeStr = arg.type;
|
|
43
|
+
// Fix imported types if needed? Usually they are unqualified if imported, or fully qualified.
|
|
44
|
+
// Pytypes usually gives "ComplexVault.Status" or "Address".
|
|
45
|
+
return `${arg.name}: ${typeStr}`;
|
|
46
|
+
});
|
|
47
|
+
// Add self as first parameter
|
|
48
|
+
const paramStr = params.length > 0 ? `self, ${params.join(', ')}` : 'self';
|
|
49
|
+
// Determine sender based on actor type
|
|
50
|
+
let senderLine;
|
|
51
|
+
if (actor === types_1.Actor.ADMIN) {
|
|
52
|
+
senderLine = ' sender = default_chain.accounts[0] # Admin';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
senderLine = ' sender = self._get_actor()';
|
|
56
|
+
}
|
|
57
|
+
// Build argument list for the contract call
|
|
58
|
+
const callArgs = method.args.map((arg) => arg.name);
|
|
59
|
+
const callArgsStr = callArgs.length > 0 ? `${callArgs.join(', ')}, ` : '';
|
|
60
|
+
// Handle different modes (using existing logic, though pytypes inspection doesn't perfectly enable this yet)
|
|
61
|
+
let methodBody = `${senderLine}
|
|
62
|
+
# self.${contractVar}.${funcName}(${callArgsStr}from_=sender)`;
|
|
63
|
+
// We wrap in try-except by default per "identical suite" robustness usually found in these tools
|
|
64
|
+
// But user said "user only need to fix the setup and add invariants" implies flows should work or fail?
|
|
65
|
+
// Let's assume standard basic call.
|
|
66
|
+
methodBody = `${senderLine}
|
|
67
|
+
try:
|
|
68
|
+
self.${contractVar}.${funcName}(${callArgsStr}from_=sender)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass`;
|
|
71
|
+
return `
|
|
72
|
+
@flow()
|
|
73
|
+
def flow_${(0, case_1.snake)(contractName)}_${(0, case_1.snake)(funcName)}(${paramStr}) -> None:
|
|
74
|
+
"""Call ${contractName}.${funcName}"""
|
|
75
|
+
${methodBody}
|
|
76
|
+
`;
|
|
77
|
+
});
|
|
78
|
+
handlebars.registerHelper('hasItems', function (arr, options) {
|
|
79
|
+
if (arr && arr.length > 0) {
|
|
80
|
+
return options.fn(this);
|
|
81
|
+
}
|
|
82
|
+
return options.inverse(this);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const actorManagerTemplate: HandlebarsTemplateDelegate<any>;
|