recon-generate 0.0.3 → 0.0.5
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 +5 -0
- package/dist/generator.d.ts +1 -0
- package/dist/generator.js +13 -5
- package/dist/index.js +81 -42
- package/dist/link.d.ts +1 -0
- package/dist/link.js +137 -0
- package/dist/templateManager.d.ts +2 -1
- package/dist/templateManager.js +8 -2
- package/dist/templates/handlebars-helpers.js +14 -5
- package/dist/templates/setup.js +15 -0
- package/dist/templates/target-functions.js +13 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Key options:
|
|
|
31
31
|
- `--exclude "A:{foo(uint256)},D"` — exclude these contracts/functions.
|
|
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
|
+
- `--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.
|
|
34
35
|
- `--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.
|
|
35
36
|
- `--force` — replace existing generated suite output under `--output` (does **not** rebuild `.recon/out`).
|
|
36
37
|
- `--force-build` — delete `.recon/out` to force a fresh compile before generation.
|
|
@@ -63,6 +64,9 @@ recon-generate --name foo --force
|
|
|
63
64
|
|
|
64
65
|
# Generate mocks and include them in targets
|
|
65
66
|
recon-generate --mock "Morpho,Other" --force
|
|
67
|
+
|
|
68
|
+
# Enable dynamic deploy helpers for Foo and Bar
|
|
69
|
+
recon-generate --dynamic-deploy "Foo,Bar" --force
|
|
66
70
|
```
|
|
67
71
|
|
|
68
72
|
### Behavior
|
|
@@ -71,6 +75,7 @@ recon-generate --mock "Morpho,Other" --force
|
|
|
71
75
|
- `--force` replaces the suite output folder; it does **not** rebuild `.recon/out`.
|
|
72
76
|
- `--force-build` deletes `.recon/out` to force a fresh compile before generation.
|
|
73
77
|
- When `--mock` is used, a fresh build is triggered, mocks are generated under `recon[-name]/mocks`, and a second build picks them up into `.recon/out` and targets.
|
|
78
|
+
- When `--dynamic-deploy` is used, generated `Setup.sol` keeps an `address[]` per listed contract, pushes the first deployment, and emits `_getRandom<Contract>` helper plus `switch<Contract>(entropy)` in `TargetFunctions.sol` to retarget to a previously deployed instance.
|
|
74
79
|
- `recon.json` is regenerated every run from current filters/contracts.
|
|
75
80
|
- Include/exclude/admin matching is tolerant of fully qualified struct names vs short names (e.g., `Morpho.MarketParams` vs `MarketParams`).
|
|
76
81
|
- Admin list sets `actor: admin` for matched functions; others default to `actor`.
|
package/dist/generator.d.ts
CHANGED
package/dist/generator.js
CHANGED
|
@@ -195,7 +195,7 @@ class ReconGenerator {
|
|
|
195
195
|
return kind;
|
|
196
196
|
}
|
|
197
197
|
async findSourceContracts() {
|
|
198
|
-
var _a;
|
|
198
|
+
var _a, _b;
|
|
199
199
|
const contracts = [];
|
|
200
200
|
const outDir = this.outDir();
|
|
201
201
|
try {
|
|
@@ -224,7 +224,12 @@ class ReconGenerator {
|
|
|
224
224
|
const lowerName = String(contractName).toLowerCase();
|
|
225
225
|
const isGeneratedMock = this.generatedMocks.has(String(contractName));
|
|
226
226
|
const contractKind = await this.getContractKind(sourcePath, String(contractName));
|
|
227
|
-
if
|
|
227
|
+
// Check if contract is explicitly included or mocked - if so, don't filter by kind
|
|
228
|
+
const isExplicitlyIncluded = this.options.include &&
|
|
229
|
+
(this.options.include.contractOnly.has(String(contractName)) ||
|
|
230
|
+
this.options.include.functions.has(String(contractName)));
|
|
231
|
+
const isExplicitlyMocked = (_b = this.options.mocks) === null || _b === void 0 ? void 0 : _b.has(String(contractName));
|
|
232
|
+
if ((contractKind === 'interface' || contractKind === 'library') && !isExplicitlyIncluded && !isExplicitlyMocked) {
|
|
228
233
|
this.logDebug('Skipping non-contract artifact', { contractName, sourcePath, contractKind });
|
|
229
234
|
continue;
|
|
230
235
|
}
|
|
@@ -388,7 +393,10 @@ class ReconGenerator {
|
|
|
388
393
|
if (!tempSrc) {
|
|
389
394
|
return;
|
|
390
395
|
}
|
|
391
|
-
const
|
|
396
|
+
const suiteDirAbs = path.isAbsolute(this.options.suiteDir)
|
|
397
|
+
? this.options.suiteDir
|
|
398
|
+
: path.join(this.foundryRoot, this.options.suiteDir);
|
|
399
|
+
const mocksDir = path.join(suiteDirAbs, 'mocks');
|
|
392
400
|
await fs.mkdir(mocksDir, { recursive: true });
|
|
393
401
|
const tempFiles = await fs.readdir(tempSrc);
|
|
394
402
|
for (const f of tempFiles) {
|
|
@@ -464,7 +472,7 @@ class ReconGenerator {
|
|
|
464
472
|
return config;
|
|
465
473
|
}
|
|
466
474
|
async run() {
|
|
467
|
-
var _a;
|
|
475
|
+
var _a, _b;
|
|
468
476
|
await this.ensureFoundryConfigExists();
|
|
469
477
|
const outPath = this.outDir();
|
|
470
478
|
const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
|
|
@@ -508,7 +516,7 @@ class ReconGenerator {
|
|
|
508
516
|
this.logDebug('No contracts left after include/exclude filters');
|
|
509
517
|
}
|
|
510
518
|
const reconConfig = await this.loadReconConfig(filteredContracts);
|
|
511
|
-
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal);
|
|
519
|
+
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_b = this.options.dynamicDeploy) !== null && _b !== void 0 ? _b : new Set());
|
|
512
520
|
await tm.generateTemplates(filteredContracts, reconConfig);
|
|
513
521
|
}
|
|
514
522
|
async listAvailable() {
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const case_1 = require("case");
|
|
40
40
|
const generator_1 = require("./generator");
|
|
41
41
|
const utils_1 = require("./utils");
|
|
42
|
+
const link_1 = require("./link");
|
|
42
43
|
function parseFilter(input) {
|
|
43
44
|
if (!input)
|
|
44
45
|
return undefined;
|
|
@@ -114,54 +115,92 @@ async function main() {
|
|
|
114
115
|
.option('--admin <spec>', 'Mark functions as admin: e.g. "A:{foo(uint256),bar()},B:{baz(address)}"')
|
|
115
116
|
.option('--name <suite>', 'Suite name; affects folder (recon-name) and Crytic artifacts')
|
|
116
117
|
.option('--mock <names>', 'Comma-separated contract names to generate mocks for')
|
|
118
|
+
.option('--dynamic-deploy <names>', 'Comma-separated contract names to enable dynamic deploy lists')
|
|
117
119
|
.option('--debug', 'Print debug info about filters and selection')
|
|
118
120
|
.option('--list', 'List available contracts/functions (after filters) and exit')
|
|
119
121
|
.option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
|
|
120
122
|
.option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
|
|
123
|
+
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)');
|
|
124
|
+
program
|
|
125
|
+
.command('link')
|
|
126
|
+
.description('Link library addresses into echidna/medusa configs via crytic-compile')
|
|
127
|
+
.option('--echidna-config <path>', 'Path to echidna yaml (defaults based on --name)')
|
|
128
|
+
.option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
|
|
129
|
+
.option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
|
|
121
130
|
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
|
|
141
|
-
const reconPath = opts.recon
|
|
142
|
-
? (path.isAbsolute(opts.recon) ? opts.recon : path.join(workspaceRoot, opts.recon))
|
|
143
|
-
: (await (0, utils_1.fileExists)(path.join(foundryRoot, reconDefaultName))
|
|
144
|
-
? path.join(foundryRoot, reconDefaultName)
|
|
145
|
-
: path.join(foundryRoot, '.recon', reconDefaultName));
|
|
146
|
-
const generator = new generator_1.ReconGenerator(foundryRoot, {
|
|
147
|
-
suiteDir,
|
|
148
|
-
reconConfigPath: reconPath,
|
|
149
|
-
foundryConfigPath: foundryConfig,
|
|
150
|
-
include: includeFilter,
|
|
151
|
-
exclude: excludeFilter,
|
|
152
|
-
admin: adminFilter ? adminFilter.functions : undefined,
|
|
153
|
-
debug: !!opts.debug,
|
|
154
|
-
force: !!opts.force,
|
|
155
|
-
forceBuild: !!opts.forceBuild,
|
|
156
|
-
mocks: mockSet,
|
|
157
|
-
suiteNameSnake: suiteSnake,
|
|
158
|
-
suiteNamePascal: suitePascal,
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
const workspaceRoot = process.cwd();
|
|
133
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
134
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
135
|
+
const suiteRaw = opts.name ? String(opts.name).trim() : '';
|
|
136
|
+
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
137
|
+
const suffix = suiteSnake ? `-${suiteSnake}` : '';
|
|
138
|
+
const echidnaDefault = `echidna${suffix}.yaml`;
|
|
139
|
+
const medusaDefault = `medusa${suffix}.json`;
|
|
140
|
+
const echidnaConfigOpt = opts.echidnaConfig || echidnaDefault;
|
|
141
|
+
const medusaConfigOpt = opts.medusaConfig || medusaDefault;
|
|
142
|
+
const echidnaConfigPath = path.isAbsolute(echidnaConfigOpt)
|
|
143
|
+
? echidnaConfigOpt
|
|
144
|
+
: path.join(foundryRoot, echidnaConfigOpt);
|
|
145
|
+
const medusaConfigPath = path.isAbsolute(medusaConfigOpt)
|
|
146
|
+
? medusaConfigOpt
|
|
147
|
+
: path.join(foundryRoot, medusaConfigOpt);
|
|
148
|
+
await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath);
|
|
159
149
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
150
|
+
program
|
|
151
|
+
.action(async (opts) => {
|
|
152
|
+
const workspaceRoot = process.cwd();
|
|
153
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
154
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
155
|
+
const includeFilter = parseFilter(opts.include);
|
|
156
|
+
const excludeFilter = parseFilter(opts.exclude);
|
|
157
|
+
const adminFilter = parseFilter(opts.admin);
|
|
158
|
+
const mockSet = opts.mock
|
|
159
|
+
? new Set(String(opts.mock).split(',').map((s) => s.trim()).filter(Boolean))
|
|
160
|
+
: undefined;
|
|
161
|
+
if (includeFilter && mockSet && mockSet.size > 0) {
|
|
162
|
+
for (const m of mockSet) {
|
|
163
|
+
includeFilter.contractOnly.add(`${m}Mock`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const dynamicDeploySet = opts.dynamicDeploy
|
|
167
|
+
? new Set(String(opts.dynamicDeploy).split(',').map((s) => s.trim()).filter(Boolean))
|
|
168
|
+
: undefined;
|
|
169
|
+
const suiteRaw = opts.name ? String(opts.name).trim() : '';
|
|
170
|
+
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
171
|
+
const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
|
|
172
|
+
const suiteFolderName = suiteSnake ? `recon-${suiteSnake}` : 'recon';
|
|
173
|
+
const baseOut = opts.output ? String(opts.output) : await (0, utils_1.getTestFolder)(foundryRoot);
|
|
174
|
+
const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.join(foundryRoot, baseOut);
|
|
175
|
+
const suiteDir = path.join(baseOutPath, suiteFolderName);
|
|
176
|
+
const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
|
|
177
|
+
const reconPath = opts.recon
|
|
178
|
+
? (path.isAbsolute(opts.recon) ? opts.recon : path.join(workspaceRoot, opts.recon))
|
|
179
|
+
: (await (0, utils_1.fileExists)(path.join(foundryRoot, reconDefaultName))
|
|
180
|
+
? path.join(foundryRoot, reconDefaultName)
|
|
181
|
+
: path.join(foundryRoot, '.recon', reconDefaultName));
|
|
182
|
+
const generator = new generator_1.ReconGenerator(foundryRoot, {
|
|
183
|
+
suiteDir,
|
|
184
|
+
reconConfigPath: reconPath,
|
|
185
|
+
foundryConfigPath: foundryConfig,
|
|
186
|
+
include: includeFilter,
|
|
187
|
+
exclude: excludeFilter,
|
|
188
|
+
admin: adminFilter ? adminFilter.functions : undefined,
|
|
189
|
+
debug: !!opts.debug,
|
|
190
|
+
force: !!opts.force,
|
|
191
|
+
forceBuild: !!opts.forceBuild,
|
|
192
|
+
mocks: mockSet,
|
|
193
|
+
dynamicDeploy: dynamicDeploySet,
|
|
194
|
+
suiteNameSnake: suiteSnake,
|
|
195
|
+
suiteNamePascal: suitePascal,
|
|
196
|
+
});
|
|
197
|
+
if (opts.list) {
|
|
198
|
+
await generator.listAvailable();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await generator.run();
|
|
202
|
+
});
|
|
203
|
+
await program.parseAsync(process.argv);
|
|
165
204
|
}
|
|
166
205
|
main().catch(err => {
|
|
167
206
|
console.error('recon-generate failed:', err.message || err);
|
package/dist/link.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLink(foundryRoot: string, echidnaConfigPath: string, medusaConfigPath: string): Promise<void>;
|
package/dist/link.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runLink = runLink;
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const fs = __importStar(require("fs/promises"));
|
|
42
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
43
|
+
const utils_1 = require("./utils");
|
|
44
|
+
const generateHexAddress = (index) => {
|
|
45
|
+
return `0xf${(index + 1).toString().padStart(2, '0')}`;
|
|
46
|
+
};
|
|
47
|
+
const parseLibrariesFromOutput = (output) => {
|
|
48
|
+
const usesPattern = /^\s+uses: \[(.*?)\]/gm;
|
|
49
|
+
const matches = [...(0, utils_1.stripAnsiCodes)(output).matchAll(usesPattern)];
|
|
50
|
+
const allLibraries = [];
|
|
51
|
+
for (const match of matches) {
|
|
52
|
+
if (match[1]) {
|
|
53
|
+
const libs = match[1]
|
|
54
|
+
.split(',')
|
|
55
|
+
.map(lib => lib.trim().replace(/["'\s]/g, ''))
|
|
56
|
+
.filter(lib => lib.length > 0);
|
|
57
|
+
for (const lib of libs) {
|
|
58
|
+
if (!allLibraries.includes(lib)) {
|
|
59
|
+
allLibraries.push(lib);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return allLibraries;
|
|
65
|
+
};
|
|
66
|
+
const runCryticCompile = (cwd) => {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
(0, child_process_1.exec)('crytic-compile . --foundry-compile-all --print-libraries', { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (error, stdout, stderr) => {
|
|
69
|
+
if (error) {
|
|
70
|
+
reject(new Error(stderr || error.message));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
resolve(stdout || '');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const formatEchidnaYaml = (config, libraries) => {
|
|
78
|
+
const cfg = { ...config };
|
|
79
|
+
delete cfg.cryticArgs;
|
|
80
|
+
delete cfg.deployContracts;
|
|
81
|
+
let out = yaml_1.default.stringify(cfg, { indent: 2 });
|
|
82
|
+
if (libraries.length === 0) {
|
|
83
|
+
out += 'cryticArgs: []\n';
|
|
84
|
+
out += 'deployContracts: []\n';
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
const libraryArgs = libraries.map((lib, index) => `(${lib},${generateHexAddress(index)})`).join(',');
|
|
88
|
+
out += `cryticArgs: ["--compile-libraries=${libraryArgs}","--foundry-compile-all"]\n`;
|
|
89
|
+
out += 'deployContracts:\n';
|
|
90
|
+
libraries.forEach((lib, index) => {
|
|
91
|
+
out += ` - ["${generateHexAddress(index)}", "${lib}"]\n`;
|
|
92
|
+
});
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
const updateEchidnaConfig = async (configPath, libraries) => {
|
|
96
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
97
|
+
const parsed = yaml_1.default.parse(content) || {};
|
|
98
|
+
const updated = formatEchidnaYaml(parsed, libraries);
|
|
99
|
+
await fs.writeFile(configPath, updated, 'utf8');
|
|
100
|
+
};
|
|
101
|
+
const updateMedusaConfig = async (configPath, libraries) => {
|
|
102
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
103
|
+
const parsed = JSON.parse(content);
|
|
104
|
+
if (!parsed.compilation)
|
|
105
|
+
parsed.compilation = {};
|
|
106
|
+
if (!parsed.compilation.platformConfig)
|
|
107
|
+
parsed.compilation.platformConfig = {};
|
|
108
|
+
if (libraries.length > 0) {
|
|
109
|
+
const libraryArgs = libraries.map((lib, index) => `(${lib},${generateHexAddress(index)})`).join(',');
|
|
110
|
+
parsed.compilation.platformConfig.args = ['--compile-libraries', libraryArgs, '--foundry-compile-all'];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
delete parsed.compilation.platformConfig.args;
|
|
114
|
+
}
|
|
115
|
+
await fs.writeFile(configPath, JSON.stringify(parsed, null, 2), 'utf8');
|
|
116
|
+
};
|
|
117
|
+
async function runLink(foundryRoot, echidnaConfigPath, medusaConfigPath) {
|
|
118
|
+
const output = await runCryticCompile(foundryRoot);
|
|
119
|
+
const libraries = parseLibrariesFromOutput(output);
|
|
120
|
+
console.log('Detected libraries:', libraries.length ? libraries.join(', ') : '(none)');
|
|
121
|
+
try {
|
|
122
|
+
await fs.access(echidnaConfigPath);
|
|
123
|
+
await updateEchidnaConfig(echidnaConfigPath, libraries);
|
|
124
|
+
console.log(`Updated echidna config at ${echidnaConfigPath}`);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
throw new Error(`Failed to update echidna config: ${e}`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(medusaConfigPath);
|
|
131
|
+
await updateMedusaConfig(medusaConfigPath, libraries);
|
|
132
|
+
console.log(`Updated medusa config at ${medusaConfigPath}`);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
throw new Error(`Failed to update medusa config: ${e}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -4,7 +4,8 @@ export declare class TemplateManager {
|
|
|
4
4
|
private suiteDir;
|
|
5
5
|
private suiteNameSnake;
|
|
6
6
|
private suiteNamePascal;
|
|
7
|
-
|
|
7
|
+
private dynamicDeploy;
|
|
8
|
+
constructor(foundryRoot: string, suiteDir: string, suiteNameSnake: string, suiteNamePascal: string, dynamicDeploy?: Set<string>);
|
|
8
9
|
private skipList;
|
|
9
10
|
private shouldGenerateFile;
|
|
10
11
|
private updateTargetFile;
|
package/dist/templateManager.js
CHANGED
|
@@ -39,11 +39,12 @@ const fs = __importStar(require("fs/promises"));
|
|
|
39
39
|
const templates = __importStar(require("./templates"));
|
|
40
40
|
const types_1 = require("./types");
|
|
41
41
|
class TemplateManager {
|
|
42
|
-
constructor(foundryRoot, suiteDir, suiteNameSnake, suiteNamePascal) {
|
|
42
|
+
constructor(foundryRoot, suiteDir, suiteNameSnake, suiteNamePascal, dynamicDeploy = new Set()) {
|
|
43
43
|
this.foundryRoot = foundryRoot;
|
|
44
44
|
this.suiteDir = suiteDir;
|
|
45
45
|
this.suiteNameSnake = suiteNameSnake;
|
|
46
46
|
this.suiteNamePascal = suiteNamePascal;
|
|
47
|
+
this.dynamicDeploy = dynamicDeploy;
|
|
47
48
|
}
|
|
48
49
|
skipList() {
|
|
49
50
|
const suiteRel = path.relative(this.foundryRoot, this.suiteDir);
|
|
@@ -180,6 +181,10 @@ class TemplateManager {
|
|
|
180
181
|
}
|
|
181
182
|
async generateTemplates(contracts, reconConfig) {
|
|
182
183
|
const { enabledContracts, adminFunctions, nonSeparatedFunctions, separatedByContract, allContractNames, } = this.collectFunctions(contracts, reconConfig);
|
|
184
|
+
const contractsForSetup = enabledContracts.map(contract => ({ ...contract, isDynamic: this.dynamicDeploy.has(contract.name) }));
|
|
185
|
+
const dynamicContracts = enabledContracts
|
|
186
|
+
.filter(contract => this.dynamicDeploy.has(contract.name))
|
|
187
|
+
.map(c => ({ name: c.name, path: c.path }));
|
|
183
188
|
const suiteRel = path.relative(this.foundryRoot, this.suiteDir);
|
|
184
189
|
const suffix = this.suiteNameSnake ? `-${this.suiteNameSnake}` : '';
|
|
185
190
|
const cryticTesterName = `CryticTester${this.suiteNamePascal}`;
|
|
@@ -193,10 +198,11 @@ class TemplateManager {
|
|
|
193
198
|
[path.join(suiteRel, `${cryticTesterName}.sol`)]: templates.cryticTesterTemplate({ cryticTesterName, suiteNameSuffix: suffix }),
|
|
194
199
|
[path.join(suiteRel, `${cryticToFoundryName}.sol`)]: templates.cryticToFoundryTemplate({ cryticToFoundryName }),
|
|
195
200
|
[path.join(suiteRel, 'Properties.sol')]: templates.propertiesTemplate({}),
|
|
196
|
-
[path.join(suiteRel, 'Setup.sol')]: templates.setupTemplate({ contracts:
|
|
201
|
+
[path.join(suiteRel, 'Setup.sol')]: templates.setupTemplate({ contracts: contractsForSetup }),
|
|
197
202
|
[path.join(suiteRel, 'TargetFunctions.sol')]: templates.targetFunctionsTemplate({
|
|
198
203
|
functions: nonSeparatedFunctions,
|
|
199
204
|
contracts: allContractNames,
|
|
205
|
+
dynamicContracts,
|
|
200
206
|
}),
|
|
201
207
|
[path.join(suiteTargetsDir, 'AdminTargets.sol')]: templates.adminTargetsTemplate({ functions: adminFunctions }),
|
|
202
208
|
[path.join(suiteTargetsDir, 'DoomsdayTargets.sol')]: templates.doomsdayTargetsTemplate({}),
|
|
@@ -86,13 +86,22 @@ function registerHelpers(handlebars) {
|
|
|
86
86
|
: `tempValue${index}`};`)
|
|
87
87
|
.join("\n ")
|
|
88
88
|
: "";
|
|
89
|
+
const inputNames = abi.inputs.map((input, index) => {
|
|
90
|
+
if (input.name && input.name.trim() !== "") {
|
|
91
|
+
return input.name;
|
|
92
|
+
}
|
|
93
|
+
if (input.type === "tuple") {
|
|
94
|
+
return `empty${index}`;
|
|
95
|
+
}
|
|
96
|
+
return `arg${index}`;
|
|
97
|
+
});
|
|
89
98
|
if (mode === types_1.Mode.NORMAL || mode === types_1.Mode.FAIL) {
|
|
90
99
|
return `
|
|
91
100
|
function ${contractName}_${abi.name}(${abi.inputs
|
|
92
|
-
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${input
|
|
101
|
+
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${inputNames[abi.inputs.indexOf(input)]}`)
|
|
93
102
|
.join(", ")}) public ${modifiersStr}{
|
|
94
103
|
${contractName}.${abi.name}${valueStr}(${abi.inputs
|
|
95
|
-
.map((input) =>
|
|
104
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
96
105
|
.join(", ")});${mode === 'fail'
|
|
97
106
|
? `
|
|
98
107
|
t(false, "${contractName}_${abi.name}");`
|
|
@@ -102,16 +111,16 @@ function registerHelpers(handlebars) {
|
|
|
102
111
|
else {
|
|
103
112
|
return `
|
|
104
113
|
function ${contractName}_${abi.name}(${abi.inputs
|
|
105
|
-
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${input
|
|
114
|
+
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${inputNames[abi.inputs.indexOf(input)]}`)
|
|
106
115
|
.join(", ")}) public ${modifiersStr}{
|
|
107
116
|
${hasOutputs ? `${outputs}
|
|
108
117
|
try ${contractName}.${abi.name}${valueStr}(${abi.inputs
|
|
109
|
-
.map((input) =>
|
|
118
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
110
119
|
.join(", ")}) returns (${returnTypes}) {
|
|
111
120
|
${assignValues}
|
|
112
121
|
}`
|
|
113
122
|
: `try ${contractName}.${abi.name}(${abi.inputs
|
|
114
|
-
.map((input) =>
|
|
123
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
115
124
|
.join(", ")}) {}`} catch {
|
|
116
125
|
${hasOutputs ? " " : " "}t(false, "${contractName}_${abi.name}");
|
|
117
126
|
${hasOutputs ? " " : " "}}
|
package/dist/templates/setup.js
CHANGED
|
@@ -29,6 +29,8 @@ import "{{this.path}}";
|
|
|
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}}address[] {{camel this.name}}_s;
|
|
33
|
+
{{/if}}
|
|
32
34
|
{{/each}}
|
|
33
35
|
|
|
34
36
|
/// === Setup === ///
|
|
@@ -36,9 +38,22 @@ abstract contract Setup is BaseSetup, ActorManager, AssetManager, Utils {
|
|
|
36
38
|
function setup() internal virtual override {
|
|
37
39
|
{{#each contracts}}
|
|
38
40
|
{{camel this.name}} = new {{this.name}}(); // TODO: Add parameters here
|
|
41
|
+
{{#if this.isDynamic}}
|
|
42
|
+
{{camel this.name}}_s.push(address({{camel this.name}}));
|
|
43
|
+
{{/if}}
|
|
39
44
|
{{/each}}
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/// === Dynamic deploy helpers === ///
|
|
48
|
+
{{#each contracts}}
|
|
49
|
+
{{#if this.isDynamic}}
|
|
50
|
+
function _getRandom{{pascal this.name}}(uint8 index) internal view returns (address) {
|
|
51
|
+
return {{camel this.name}}_s[index % {{camel this.name}}_s.length];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
{{/if}}
|
|
55
|
+
{{/each}}
|
|
56
|
+
|
|
42
57
|
/// === MODIFIERS === ///
|
|
43
58
|
/// Prank admin and actor
|
|
44
59
|
|
|
@@ -22,6 +22,11 @@ import {Panic} from "@recon/Panic.sol";
|
|
|
22
22
|
import { {{pascal this}}Targets } from "./targets/{{pascal this}}Targets.sol";
|
|
23
23
|
{{/each}}
|
|
24
24
|
|
|
25
|
+
// Dynamic deploy contract types
|
|
26
|
+
{{#each dynamicContracts}}
|
|
27
|
+
import "{{this.path}}";
|
|
28
|
+
{{/each}}
|
|
29
|
+
|
|
25
30
|
abstract contract TargetFunctions is
|
|
26
31
|
{{#each contracts}}{{pascal this}}Targets{{#unless @last}},
|
|
27
32
|
{{/unless}}{{/each}}
|
|
@@ -32,6 +37,14 @@ abstract contract TargetFunctions is
|
|
|
32
37
|
/// AUTO GENERATED TARGET FUNCTIONS - WARNING: DO NOT DELETE OR MODIFY THIS LINE ///
|
|
33
38
|
{{#each functions}}
|
|
34
39
|
{{functionDefinition this}}
|
|
40
|
+
{{/each}}
|
|
41
|
+
|
|
42
|
+
/// AUTO GENERATED DYNAMIC DEPLOY SWITCHES ///
|
|
43
|
+
{{#each dynamicContracts}}
|
|
44
|
+
function switch{{pascal this.name}}(uint8 entropy) public {
|
|
45
|
+
{{camel this.name}} = {{this.name}}(_getRandom{{pascal this.name}}(entropy));
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
{{/each}}
|
|
36
49
|
}
|
|
37
50
|
`, { noEscape: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recon-generate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"abi-to-mock": "^1.0.4",
|
|
23
23
|
"case": "^1.6.3",
|
|
24
24
|
"commander": "^14.0.2",
|
|
25
|
-
"handlebars": "^4.7.8"
|
|
25
|
+
"handlebars": "^4.7.8",
|
|
26
|
+
"yaml": "^2.8.2"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^24.0.4",
|