recon-generate 0.0.33 → 0.0.34
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/dictionary.js +2 -1
- package/dist/info.js +2 -1
- package/dist/link2.js +2 -1
- package/dist/sourcemap.js +6 -4
- package/dist/templateManager.d.ts +2 -2
- package/dist/templateManager.js +109 -41
- package/dist/templates/before-after.js +15 -0
- package/dist/templates/handlebars-helpers.js +55 -18
- package/dist/templates/index.d.ts +16 -14
- package/dist/templates/index.js +5 -1
- package/dist/templates/readme.d.ts +1 -0
- package/dist/templates/readme.js +158 -0
- package/dist/templates/selector-storage.d.ts +1 -0
- package/dist/templates/selector-storage.js +19 -0
- package/dist/templates/target-functions.js +2 -0
- package/dist/templates/targets/admin-targets.js +2 -0
- package/dist/templates/targets/targets.js +2 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +0 -2
- package/package.json +1 -1
package/dist/dictionary.js
CHANGED
|
@@ -160,7 +160,8 @@ const runDictionary = async (foundryRoot, options = {}) => {
|
|
|
160
160
|
const log = options.json ? () => { } : console.log.bind(console);
|
|
161
161
|
const srcPrefix = (_a = options.srcPrefix) !== null && _a !== void 0 ? _a : 'src/';
|
|
162
162
|
log(`[recon-generate] Loading build-info from ${foundryRoot}`);
|
|
163
|
-
const
|
|
163
|
+
const foundryConfig = await (0, utils_1.getFoundryConfig)(foundryRoot);
|
|
164
|
+
const { sourceUnits } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot, foundryConfig.out || 'out');
|
|
164
165
|
if (!sourceUnits || sourceUnits.length === 0) {
|
|
165
166
|
throw new Error('No source units were produced from the build; cannot generate dictionary.');
|
|
166
167
|
}
|
package/dist/info.js
CHANGED
|
@@ -922,7 +922,8 @@ const runInfo = async (foundryRoot, contractName, options = {}) => {
|
|
|
922
922
|
if (options.json) {
|
|
923
923
|
(0, z3Solver_1.setZ3Silent)(true);
|
|
924
924
|
}
|
|
925
|
-
const
|
|
925
|
+
const foundryConfig = await (0, utils_1.getFoundryConfig)(foundryRoot);
|
|
926
|
+
const { sourceUnits, buildOutput } = await (0, utils_1.loadLatestBuildInfo)(foundryRoot, foundryConfig.out || 'out');
|
|
926
927
|
if (!sourceUnits || sourceUnits.length === 0) {
|
|
927
928
|
throw new Error('No source units were produced from the build; cannot generate info.');
|
|
928
929
|
}
|
package/dist/link2.js
CHANGED
|
@@ -95,7 +95,8 @@ const runForgeBuild = async (cwd, verbose = false) => {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
// Find and read the most recent build-info JSON file
|
|
98
|
-
const
|
|
98
|
+
const foundryConfig = await (0, utils_1.getFoundryConfig)(cwd);
|
|
99
|
+
const buildInfoDir = path.join(cwd, foundryConfig.out, 'build-info');
|
|
99
100
|
if (verbose) {
|
|
100
101
|
console.log(`[VERBOSE] Looking for build-info files in: ${buildInfoDir}`);
|
|
101
102
|
}
|
package/dist/sourcemap.js
CHANGED
|
@@ -70,8 +70,8 @@ function findFoundryProject(startDir = process.cwd()) {
|
|
|
70
70
|
}
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
|
-
function findBuildInfo(projectDir) {
|
|
74
|
-
const buildInfoDir = path.join(projectDir,
|
|
73
|
+
function findBuildInfo(projectDir, outDir = 'out') {
|
|
74
|
+
const buildInfoDir = path.join(projectDir, outDir, 'build-info');
|
|
75
75
|
if (!fs.existsSync(buildInfoDir))
|
|
76
76
|
return null;
|
|
77
77
|
const files = fs.readdirSync(buildInfoDir).filter(f => f.endsWith('.json'));
|
|
@@ -153,13 +153,15 @@ async function runSourcemap(foundryRoot, options = {}) {
|
|
|
153
153
|
console.log(`[VERBOSE] Foundry root: ${foundryRoot}`);
|
|
154
154
|
console.log(`[VERBOSE] Output dir: ${outputDir}`);
|
|
155
155
|
}
|
|
156
|
+
const foundryConfig = await (0, utils_1.getFoundryConfig)(projectDir);
|
|
157
|
+
const forgeOutDir = foundryConfig.out || 'out';
|
|
156
158
|
console.log(`📁 Project: ${projectDir}`);
|
|
157
159
|
console.log(`📂 Output: ${outputDir}`);
|
|
158
160
|
const startTime = Date.now();
|
|
159
161
|
// Find build info
|
|
160
|
-
const buildInfoPath = findBuildInfo(projectDir);
|
|
162
|
+
const buildInfoPath = findBuildInfo(projectDir, forgeOutDir);
|
|
161
163
|
if (!buildInfoPath) {
|
|
162
|
-
throw new Error(`No build-info found in ${projectDir}/
|
|
164
|
+
throw new Error(`No build-info found in ${projectDir}/${forgeOutDir}/build-info/. Run "forge build" first to generate build artifacts.`);
|
|
163
165
|
}
|
|
164
166
|
console.log(`📂 Found build-info: ${path.basename(buildInfoPath)}`);
|
|
165
167
|
// Load AST
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FunctionDefinitionParams, ContractMetadata } from
|
|
2
|
-
import { SymbolsInfo } from
|
|
1
|
+
import { FunctionDefinitionParams, ContractMetadata } from "./types";
|
|
2
|
+
import { SymbolsInfo } from "./utils";
|
|
3
3
|
export declare class TemplateManager {
|
|
4
4
|
private foundryRoot;
|
|
5
5
|
private suiteDir;
|
package/dist/templateManager.js
CHANGED
|
@@ -49,25 +49,26 @@ class TemplateManager {
|
|
|
49
49
|
}
|
|
50
50
|
skipList() {
|
|
51
51
|
const suiteRel = path.relative(this.foundryRoot, this.suiteDir);
|
|
52
|
-
const suffix = this.suiteNameSnake ? `-${this.suiteNameSnake}` :
|
|
52
|
+
const suffix = this.suiteNameSnake ? `-${this.suiteNameSnake}` : "";
|
|
53
53
|
const cryticTesterName = `CryticTester${this.suiteNamePascal}`;
|
|
54
54
|
const cryticToFoundryName = `CryticToFoundry${this.suiteNamePascal}`;
|
|
55
55
|
return [
|
|
56
56
|
`echidna${suffix}.yaml`,
|
|
57
57
|
`medusa${suffix}.json`,
|
|
58
58
|
`halmos${suffix}.toml`,
|
|
59
|
-
path.join(suiteRel,
|
|
59
|
+
path.join(suiteRel, "BeforeAfter.sol"),
|
|
60
|
+
path.join(suiteRel, "SelectorStorage.sol"),
|
|
60
61
|
path.join(suiteRel, `${cryticTesterName}.sol`),
|
|
61
62
|
path.join(suiteRel, `${cryticToFoundryName}.sol`),
|
|
62
|
-
path.join(suiteRel,
|
|
63
|
-
path.join(suiteRel,
|
|
64
|
-
path.join(suiteRel,
|
|
65
|
-
path.join(suiteRel,
|
|
66
|
-
path.join(suiteRel,
|
|
67
|
-
path.join(suiteRel,
|
|
68
|
-
path.join(suiteRel,
|
|
69
|
-
path.join(suiteRel,
|
|
70
|
-
path.join(suiteRel,
|
|
63
|
+
path.join(suiteRel, "Properties.sol"),
|
|
64
|
+
path.join(suiteRel, "helpers/Utils.sol"),
|
|
65
|
+
path.join(suiteRel, "helpers/Panic.sol"),
|
|
66
|
+
path.join(suiteRel, "managers/ActorManager.sol"),
|
|
67
|
+
path.join(suiteRel, "managers/AssetManager.sol"),
|
|
68
|
+
path.join(suiteRel, "managers/utils/EnumerableSet.sol"),
|
|
69
|
+
path.join(suiteRel, "mocks/MockERC20.sol"),
|
|
70
|
+
path.join(suiteRel, "targets/DoomsdayTargets.sol"),
|
|
71
|
+
path.join(suiteRel, "targets/ManagersTargets.sol"),
|
|
71
72
|
];
|
|
72
73
|
}
|
|
73
74
|
async shouldGenerateFile(filePath) {
|
|
@@ -75,7 +76,9 @@ class TemplateManager {
|
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
77
78
|
try {
|
|
78
|
-
const targetPath = path.isAbsolute(filePath)
|
|
79
|
+
const targetPath = path.isAbsolute(filePath)
|
|
80
|
+
? filePath
|
|
81
|
+
: path.join(this.foundryRoot, filePath);
|
|
79
82
|
await fs.access(targetPath);
|
|
80
83
|
return false;
|
|
81
84
|
}
|
|
@@ -85,11 +88,13 @@ class TemplateManager {
|
|
|
85
88
|
}
|
|
86
89
|
async updateTargetFile(filepath, newContent) {
|
|
87
90
|
try {
|
|
88
|
-
const existingContent = await fs.readFile(filepath,
|
|
89
|
-
if (existingContent.includes(
|
|
90
|
-
const [beforeMarker, afterMarker] = existingContent.split(
|
|
91
|
-
const [, newAutoGenerated] = newContent.split(
|
|
92
|
-
return beforeMarker +
|
|
91
|
+
const existingContent = await fs.readFile(filepath, "utf8");
|
|
92
|
+
if (existingContent.includes("/// AUTO GENERATED TARGET FUNCTIONS - WARNING: DO NOT DELETE OR MODIFY THIS LINE ///")) {
|
|
93
|
+
const [beforeMarker, afterMarker] = existingContent.split("/// AUTO GENERATED TARGET FUNCTIONS - WARNING: DO NOT DELETE OR MODIFY THIS LINE ///");
|
|
94
|
+
const [, newAutoGenerated] = newContent.split("/// AUTO GENERATED TARGET FUNCTIONS - WARNING: DO NOT DELETE OR MODIFY THIS LINE ///");
|
|
95
|
+
return (beforeMarker +
|
|
96
|
+
"/// AUTO GENERATED TARGET FUNCTIONS - WARNING: DO NOT DELETE OR MODIFY THIS LINE ///" +
|
|
97
|
+
newAutoGenerated);
|
|
93
98
|
}
|
|
94
99
|
return newContent;
|
|
95
100
|
}
|
|
@@ -98,39 +103,100 @@ class TemplateManager {
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
async generateTemplates(enabledContracts, adminFunctions, nonSeparatedFunctions, separatedByContract, allContractNames, symbolsInfo) {
|
|
101
|
-
const contractsForSetup = enabledContracts.map(contract => ({
|
|
106
|
+
const contractsForSetup = enabledContracts.map((contract) => ({
|
|
107
|
+
...contract,
|
|
108
|
+
isDynamic: this.dynamicDeploy.has(contract.name),
|
|
109
|
+
}));
|
|
102
110
|
const dynamicContracts = enabledContracts
|
|
103
|
-
.filter(contract => this.dynamicDeploy.has(contract.name))
|
|
104
|
-
.map(c => ({ name: c.name, path: c.path }));
|
|
111
|
+
.filter((contract) => this.dynamicDeploy.has(contract.name))
|
|
112
|
+
.map((c) => ({ name: c.name, path: c.path }));
|
|
105
113
|
const suiteRel = path.relative(this.foundryRoot, this.suiteDir);
|
|
106
|
-
const suffix = this.suiteNameSnake ? `-${this.suiteNameSnake}` :
|
|
114
|
+
const suffix = this.suiteNameSnake ? `-${this.suiteNameSnake}` : "";
|
|
107
115
|
const cryticTesterName = `CryticTester${this.suiteNamePascal}`;
|
|
108
116
|
const cryticToFoundryName = `CryticToFoundry${this.suiteNamePascal}`;
|
|
109
|
-
const suiteTargetsDir = path.join(suiteRel,
|
|
117
|
+
const suiteTargetsDir = path.join(suiteRel, "targets");
|
|
118
|
+
// Collect all functions to count overloads
|
|
119
|
+
const allFunctionsRaw = [
|
|
120
|
+
...adminFunctions,
|
|
121
|
+
...nonSeparatedFunctions,
|
|
122
|
+
...Object.values(separatedByContract).flat(),
|
|
123
|
+
];
|
|
124
|
+
// Simple overload tracking with plain objects
|
|
125
|
+
const counts = {};
|
|
126
|
+
for (const fn of allFunctionsRaw) {
|
|
127
|
+
const key = `${fn.contractName}_${fn.abi.name}`;
|
|
128
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
129
|
+
}
|
|
130
|
+
// Helper to add overloadIndex to a function array
|
|
131
|
+
const addOverloadIndices = (functions) => {
|
|
132
|
+
const indices = {};
|
|
133
|
+
return functions.map((fn) => {
|
|
134
|
+
const key = `${fn.contractName}_${fn.abi.name}`;
|
|
135
|
+
if (counts[key] > 1) {
|
|
136
|
+
const idx = indices[key] || 0;
|
|
137
|
+
indices[key] = idx + 1;
|
|
138
|
+
return { ...fn, overloadIndex: idx };
|
|
139
|
+
}
|
|
140
|
+
return fn;
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
// Apply overload indexing to all function arrays
|
|
144
|
+
const indexedAdminFunctions = addOverloadIndices(adminFunctions);
|
|
145
|
+
const indexedNonSeparatedFunctions = addOverloadIndices(nonSeparatedFunctions);
|
|
146
|
+
const indexedSeparatedByContract = {};
|
|
147
|
+
for (const [contractName, functions] of Object.entries(separatedByContract)) {
|
|
148
|
+
indexedSeparatedByContract[contractName] = addOverloadIndices(functions);
|
|
149
|
+
}
|
|
150
|
+
// All functions with overload indices for SelectorStorage
|
|
151
|
+
const allFunctions = [
|
|
152
|
+
...indexedAdminFunctions,
|
|
153
|
+
...indexedNonSeparatedFunctions,
|
|
154
|
+
...Object.values(indexedSeparatedByContract).flat(),
|
|
155
|
+
];
|
|
110
156
|
const files = {
|
|
111
|
-
[`echidna${suffix}.yaml`]: templates.echidnaConfigTemplate({
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
[
|
|
115
|
-
|
|
157
|
+
[`echidna${suffix}.yaml`]: templates.echidnaConfigTemplate({
|
|
158
|
+
corpusDir: `echidna${suffix}`,
|
|
159
|
+
}),
|
|
160
|
+
[`medusa${suffix}.json`]: templates.medusaConfigTemplate({
|
|
161
|
+
cryticTesterName,
|
|
162
|
+
}),
|
|
163
|
+
[`halmos${suffix}.toml`]: templates.halmosConfigTemplate({
|
|
164
|
+
cryticToFoundryName,
|
|
165
|
+
}),
|
|
166
|
+
[path.join(suiteRel, "BeforeAfter.sol")]: templates.beforeAfterTemplate({}),
|
|
167
|
+
[path.join(suiteRel, "SelectorStorage.sol")]: templates.selectorStorageTemplate({
|
|
168
|
+
functions: allFunctions,
|
|
169
|
+
}),
|
|
170
|
+
[path.join(suiteRel, `${cryticTesterName}.sol`)]: templates.cryticTesterTemplate({
|
|
171
|
+
cryticTesterName,
|
|
172
|
+
suiteNameSuffix: suffix,
|
|
173
|
+
}),
|
|
116
174
|
[path.join(suiteRel, `${cryticToFoundryName}.sol`)]: templates.cryticToFoundryTemplate({ cryticToFoundryName }),
|
|
117
|
-
[path.join(suiteRel,
|
|
118
|
-
[path.join(suiteRel,
|
|
119
|
-
|
|
120
|
-
|
|
175
|
+
[path.join(suiteRel, "Properties.sol")]: templates.propertiesTemplate({}),
|
|
176
|
+
[path.join(suiteRel, "Setup.sol")]: templates.setupTemplate({
|
|
177
|
+
contracts: contractsForSetup,
|
|
178
|
+
}),
|
|
179
|
+
[path.join(suiteRel, "TargetFunctions.sol")]: templates.targetFunctionsTemplate({
|
|
180
|
+
functions: indexedNonSeparatedFunctions,
|
|
121
181
|
contracts: allContractNames,
|
|
122
182
|
dynamicContracts,
|
|
123
183
|
}),
|
|
124
|
-
[path.join(suiteTargetsDir,
|
|
125
|
-
functions:
|
|
126
|
-
extraImports: symbolsInfo ? (0, utils_1.resolveExtraImports)([],
|
|
184
|
+
[path.join(suiteTargetsDir, "AdminTargets.sol")]: templates.adminTargetsTemplate({
|
|
185
|
+
functions: indexedAdminFunctions,
|
|
186
|
+
extraImports: symbolsInfo ? (0, utils_1.resolveExtraImports)([], indexedAdminFunctions, symbolsInfo) : [],
|
|
187
|
+
}),
|
|
188
|
+
[path.join(suiteTargetsDir, "DoomsdayTargets.sol")]: templates.doomsdayTargetsTemplate({}),
|
|
189
|
+
[path.join(suiteTargetsDir, "ManagersTargets.sol")]: templates.managersTargetsTemplate({}),
|
|
190
|
+
[path.join(suiteRel, "README.md")]: templates.readmeTemplate({
|
|
191
|
+
suiteFolder: path.basename(this.suiteDir),
|
|
192
|
+
cryticTesterName,
|
|
193
|
+
cryticToFoundryName,
|
|
194
|
+
configSuffix: suffix,
|
|
127
195
|
}),
|
|
128
|
-
[path.join(suiteTargetsDir, 'DoomsdayTargets.sol')]: templates.doomsdayTargetsTemplate({}),
|
|
129
|
-
[path.join(suiteTargetsDir, 'ManagersTargets.sol')]: templates.managersTargetsTemplate({}),
|
|
130
196
|
};
|
|
131
|
-
for (const [contractName, functions] of Object.entries(
|
|
197
|
+
for (const [contractName, functions] of Object.entries(indexedSeparatedByContract)) {
|
|
132
198
|
const targetPath = path.join(suiteTargetsDir, `${(0, case_1.pascal)(contractName)}Targets.sol`);
|
|
133
|
-
const contractPath = functions.length > 0 ? functions[0].contractPath :
|
|
199
|
+
const contractPath = functions.length > 0 ? functions[0].contractPath : "";
|
|
134
200
|
const importedPaths = contractPath ? [contractPath] : [];
|
|
135
201
|
files[targetPath] = templates.targetsTemplate({
|
|
136
202
|
contractName,
|
|
@@ -140,19 +206,21 @@ class TemplateManager {
|
|
|
140
206
|
});
|
|
141
207
|
}
|
|
142
208
|
for (const [name, content] of Object.entries(files)) {
|
|
143
|
-
const outputPath = path.isAbsolute(name)
|
|
209
|
+
const outputPath = path.isAbsolute(name)
|
|
210
|
+
? name
|
|
211
|
+
: path.join(this.foundryRoot, name);
|
|
144
212
|
if (!(await this.shouldGenerateFile(name))) {
|
|
145
213
|
continue;
|
|
146
214
|
}
|
|
147
215
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
148
|
-
if (name.endsWith(
|
|
216
|
+
if (name.endsWith("Setup.sol")) {
|
|
149
217
|
try {
|
|
150
218
|
await fs.access(outputPath);
|
|
151
219
|
continue; // do not overwrite user setup
|
|
152
220
|
}
|
|
153
221
|
catch { }
|
|
154
222
|
}
|
|
155
|
-
if (name.includes(
|
|
223
|
+
if (name.includes("targets/") && name.endsWith("Targets.sol")) {
|
|
156
224
|
const finalContent = await this.updateTargetFile(outputPath, content);
|
|
157
225
|
await fs.writeFile(outputPath, finalContent);
|
|
158
226
|
}
|
|
@@ -11,9 +11,15 @@ exports.beforeAfterTemplate = handlebars_1.default.compile(`// SPDX-License-Iden
|
|
|
11
11
|
pragma solidity ^0.8.0;
|
|
12
12
|
|
|
13
13
|
import {Setup} from "./Setup.sol";
|
|
14
|
+
import {SelectorStorage} from "./SelectorStorage.sol";
|
|
14
15
|
|
|
15
16
|
// ghost variables for tracking state variable values before and after function calls
|
|
16
17
|
abstract contract BeforeAfter is Setup {
|
|
18
|
+
|
|
19
|
+
// Tracks which function selector is currently executing
|
|
20
|
+
// Using bytes4 selector instead of enum to avoid 256 member limit
|
|
21
|
+
bytes4 public currentOperation;
|
|
22
|
+
|
|
17
23
|
struct Vars {
|
|
18
24
|
uint256 __ignore__;
|
|
19
25
|
}
|
|
@@ -27,6 +33,15 @@ abstract contract BeforeAfter is Setup {
|
|
|
27
33
|
__after();
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
|
|
37
|
+
// Sets currentOperation to the function selector before execution
|
|
38
|
+
modifier trackOp(bytes4 op) {
|
|
39
|
+
currentOperation = op;
|
|
40
|
+
__before();
|
|
41
|
+
_;
|
|
42
|
+
__after();
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
function __before() internal {
|
|
31
46
|
|
|
32
47
|
}
|
|
@@ -31,13 +31,17 @@ function getUsableInternalType(internalType) {
|
|
|
31
31
|
}
|
|
32
32
|
function conditionallyAddMemoryLocation(type, internalType) {
|
|
33
33
|
if (type.indexOf("[") !== -1) {
|
|
34
|
-
if (internalType.startsWith("struct ") ||
|
|
34
|
+
if (internalType.startsWith("struct ") ||
|
|
35
|
+
internalType.startsWith("enum ") ||
|
|
36
|
+
internalType.startsWith("contract ")) {
|
|
35
37
|
const usable = getUsableInternalType(internalType);
|
|
36
38
|
return `${usable} memory`;
|
|
37
39
|
}
|
|
38
40
|
return `${type} memory`;
|
|
39
41
|
}
|
|
40
|
-
if (internalType.startsWith("struct ") ||
|
|
42
|
+
if (internalType.startsWith("struct ") ||
|
|
43
|
+
internalType.startsWith("enum ") ||
|
|
44
|
+
internalType.startsWith("contract ")) {
|
|
41
45
|
return `${getUsableInternalType(internalType)}${internalType.startsWith("struct ") ? " memory" : ""}`;
|
|
42
46
|
}
|
|
43
47
|
if (type === "bytes" || type === "string") {
|
|
@@ -51,37 +55,69 @@ function extractType(inputOrOutput) {
|
|
|
51
55
|
}
|
|
52
56
|
return inputOrOutput.type;
|
|
53
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Recursively expands a ParamDefinition to its canonical ABI type string.
|
|
60
|
+
* Tuple types (structs) are expanded to "(type1,type2,...)" using components.
|
|
61
|
+
* Array suffixes like "[]" or "[5]" are preserved.
|
|
62
|
+
*/
|
|
63
|
+
function canonicalParamType(param) {
|
|
64
|
+
if (!param.type.startsWith("tuple") || !param.components || param.components.length === 0) {
|
|
65
|
+
return param.type;
|
|
66
|
+
}
|
|
67
|
+
// Extract array suffix: "tuple[]" -> "[]", "tuple[5]" -> "[5]", "tuple" -> ""
|
|
68
|
+
const arraySuffix = param.type.slice("tuple".length);
|
|
69
|
+
const inner = param.components.map((c) => canonicalParamType(c)).join(",");
|
|
70
|
+
return `(${inner})${arraySuffix}`;
|
|
71
|
+
}
|
|
54
72
|
function registerHelpers(handlebars) {
|
|
55
|
-
handlebars.registerHelper(
|
|
73
|
+
handlebars.registerHelper("snake", function (str) {
|
|
56
74
|
return (0, case_1.snake)(str);
|
|
57
75
|
});
|
|
58
|
-
handlebars.registerHelper(
|
|
76
|
+
handlebars.registerHelper("scream", function (str) {
|
|
59
77
|
return (0, case_1.snake)(str).toUpperCase();
|
|
60
78
|
});
|
|
61
|
-
handlebars.registerHelper(
|
|
79
|
+
handlebars.registerHelper("camel", function (str) {
|
|
62
80
|
return (0, case_1.camel)(str);
|
|
63
81
|
});
|
|
64
|
-
handlebars.registerHelper(
|
|
82
|
+
handlebars.registerHelper("pascal", function (str) {
|
|
65
83
|
return (0, case_1.pascal)(str);
|
|
66
84
|
});
|
|
67
|
-
handlebars.registerHelper(
|
|
85
|
+
handlebars.registerHelper("instanceName", function (str) {
|
|
68
86
|
return safeInstanceName(str);
|
|
69
87
|
});
|
|
70
|
-
|
|
88
|
+
// Selector constant name: CONTRACT_FUNCTION or CONTRACT_FUNCTION_0 for overloads
|
|
89
|
+
handlebars.registerHelper("selectorConstName", function (fn) {
|
|
90
|
+
const contract = (0, case_1.snake)(fn.contractName).toUpperCase();
|
|
91
|
+
const func = (0, case_1.snake)(fn.abi.name).toUpperCase();
|
|
92
|
+
const suffix = fn.overloadIndex !== undefined ? `_${fn.overloadIndex}` : "";
|
|
93
|
+
return `${contract}_${func}${suffix}`;
|
|
94
|
+
});
|
|
95
|
+
// Function signature: "functionName(type1,type2)"
|
|
96
|
+
// Recursively expands tuple types using components for correct keccak256 selectors
|
|
97
|
+
handlebars.registerHelper("functionSignature", function (fn) {
|
|
98
|
+
const paramTypes = fn.abi.inputs.map((input) => canonicalParamType(input)).join(",");
|
|
99
|
+
return `${fn.abi.name}(${paramTypes})`;
|
|
100
|
+
});
|
|
101
|
+
handlebars.registerHelper("functionDefinition", function ({ contractName, abi, actor, mode, overloadIndex }) {
|
|
71
102
|
const funcPrefix = (0, case_1.camel)(contractName); // For function names: fB_method
|
|
72
103
|
const instanceVar = safeInstanceName(contractName); // For instance access: _fB.method
|
|
73
104
|
let modifiers = [];
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
// Use constant from SelectorStorage
|
|
106
|
+
const contract = (0, case_1.snake)(contractName).toUpperCase();
|
|
107
|
+
const func = (0, case_1.snake)(abi.name).toUpperCase();
|
|
108
|
+
const suffix = overloadIndex !== undefined ? `_${overloadIndex}` : "";
|
|
109
|
+
modifiers.push(`trackOp(SelectorStorage.${contract}_${func}${suffix})`);
|
|
110
|
+
if (abi.stateMutability === "payable") {
|
|
111
|
+
modifiers.push("payable");
|
|
76
112
|
}
|
|
77
113
|
if (actor === types_1.Actor.ADMIN) {
|
|
78
|
-
modifiers.push(
|
|
114
|
+
modifiers.push("asAdmin");
|
|
79
115
|
}
|
|
80
116
|
else if (actor === types_1.Actor.ACTOR) {
|
|
81
|
-
modifiers.push(
|
|
117
|
+
modifiers.push("asActor");
|
|
82
118
|
}
|
|
83
|
-
const valueStr = abi.stateMutability ===
|
|
84
|
-
const modifiersStr = modifiers.length ? modifiers.join(
|
|
119
|
+
const valueStr = abi.stateMutability === "payable" ? "{value: msg.value}" : "";
|
|
120
|
+
const modifiersStr = modifiers.length ? modifiers.join(" ") + " " : "";
|
|
85
121
|
const hasOutputs = abi.outputs && abi.outputs.length > 0;
|
|
86
122
|
const outputs = abi.outputs && abi.outputs.length > 0
|
|
87
123
|
? abi.outputs
|
|
@@ -118,7 +154,7 @@ function registerHelpers(handlebars) {
|
|
|
118
154
|
.join(", ")}) public ${modifiersStr}{
|
|
119
155
|
${instanceVar}.${abi.name}${valueStr}(${abi.inputs
|
|
120
156
|
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
121
|
-
.join(", ")});${mode ===
|
|
157
|
+
.join(", ")});${mode === "fail"
|
|
122
158
|
? `
|
|
123
159
|
t(false, "${funcPrefix}_${abi.name}");`
|
|
124
160
|
: ""}
|
|
@@ -129,10 +165,11 @@ function registerHelpers(handlebars) {
|
|
|
129
165
|
function ${funcPrefix}_${abi.name}(${abi.inputs
|
|
130
166
|
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${inputNames[abi.inputs.indexOf(input)]}`)
|
|
131
167
|
.join(", ")}) public ${modifiersStr}{
|
|
132
|
-
${hasOutputs
|
|
168
|
+
${hasOutputs
|
|
169
|
+
? `${outputs}
|
|
133
170
|
try ${instanceVar}.${abi.name}${valueStr}(${abi.inputs
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
172
|
+
.join(", ")}) returns (${returnTypes}) {
|
|
136
173
|
${assignValues}
|
|
137
174
|
}`
|
|
138
175
|
: `try ${instanceVar}.${abi.name}(${abi.inputs
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { echidnaConfigTemplate } from
|
|
2
|
-
import { halmosConfigTemplate } from
|
|
3
|
-
import { medusaConfigTemplate } from
|
|
4
|
-
import { beforeAfterTemplate } from
|
|
5
|
-
import { cryticTesterTemplate } from
|
|
6
|
-
import { cryticToFoundryTemplate } from
|
|
7
|
-
import { propertiesTemplate } from
|
|
8
|
-
import { setupTemplate } from
|
|
9
|
-
import { targetFunctionsTemplate } from
|
|
10
|
-
import { adminTargetsTemplate } from
|
|
11
|
-
import { doomsdayTargetsTemplate } from
|
|
12
|
-
import { managersTargetsTemplate } from
|
|
13
|
-
import { targetsTemplate } from
|
|
14
|
-
|
|
1
|
+
import { echidnaConfigTemplate } from "./echidna-config";
|
|
2
|
+
import { halmosConfigTemplate } from "./halmos-config";
|
|
3
|
+
import { medusaConfigTemplate } from "./medusa-config";
|
|
4
|
+
import { beforeAfterTemplate } from "./before-after";
|
|
5
|
+
import { cryticTesterTemplate } from "./crytic-tester";
|
|
6
|
+
import { cryticToFoundryTemplate } from "./crytic-to-foundry";
|
|
7
|
+
import { propertiesTemplate } from "./properties";
|
|
8
|
+
import { setupTemplate } from "./setup";
|
|
9
|
+
import { targetFunctionsTemplate } from "./target-functions";
|
|
10
|
+
import { adminTargetsTemplate } from "./targets/admin-targets";
|
|
11
|
+
import { doomsdayTargetsTemplate } from "./targets/doomsday-targets";
|
|
12
|
+
import { managersTargetsTemplate } from "./targets/managers-targets";
|
|
13
|
+
import { targetsTemplate } from "./targets/targets";
|
|
14
|
+
import { selectorStorageTemplate } from "./selector-storage";
|
|
15
|
+
import { readmeTemplate } from "./readme";
|
|
16
|
+
export { echidnaConfigTemplate, halmosConfigTemplate, medusaConfigTemplate, beforeAfterTemplate, cryticTesterTemplate, cryticToFoundryTemplate, propertiesTemplate, setupTemplate, targetFunctionsTemplate, adminTargetsTemplate, doomsdayTargetsTemplate, managersTargetsTemplate, targetsTemplate, selectorStorageTemplate, readmeTemplate, };
|
package/dist/templates/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.targetsTemplate = exports.managersTargetsTemplate = exports.doomsdayTargetsTemplate = exports.adminTargetsTemplate = exports.targetFunctionsTemplate = exports.setupTemplate = exports.propertiesTemplate = exports.cryticToFoundryTemplate = exports.cryticTesterTemplate = exports.beforeAfterTemplate = exports.medusaConfigTemplate = exports.halmosConfigTemplate = exports.echidnaConfigTemplate = void 0;
|
|
3
|
+
exports.readmeTemplate = exports.selectorStorageTemplate = exports.targetsTemplate = exports.managersTargetsTemplate = exports.doomsdayTargetsTemplate = exports.adminTargetsTemplate = exports.targetFunctionsTemplate = exports.setupTemplate = exports.propertiesTemplate = exports.cryticToFoundryTemplate = exports.cryticTesterTemplate = exports.beforeAfterTemplate = exports.medusaConfigTemplate = exports.halmosConfigTemplate = exports.echidnaConfigTemplate = void 0;
|
|
4
4
|
const echidna_config_1 = require("./echidna-config");
|
|
5
5
|
Object.defineProperty(exports, "echidnaConfigTemplate", { enumerable: true, get: function () { return echidna_config_1.echidnaConfigTemplate; } });
|
|
6
6
|
const halmos_config_1 = require("./halmos-config");
|
|
@@ -27,3 +27,7 @@ const managers_targets_1 = require("./targets/managers-targets");
|
|
|
27
27
|
Object.defineProperty(exports, "managersTargetsTemplate", { enumerable: true, get: function () { return managers_targets_1.managersTargetsTemplate; } });
|
|
28
28
|
const targets_1 = require("./targets/targets");
|
|
29
29
|
Object.defineProperty(exports, "targetsTemplate", { enumerable: true, get: function () { return targets_1.targetsTemplate; } });
|
|
30
|
+
const selector_storage_1 = require("./selector-storage");
|
|
31
|
+
Object.defineProperty(exports, "selectorStorageTemplate", { enumerable: true, get: function () { return selector_storage_1.selectorStorageTemplate; } });
|
|
32
|
+
const readme_1 = require("./readme");
|
|
33
|
+
Object.defineProperty(exports, "readmeTemplate", { enumerable: true, get: function () { return readme_1.readmeTemplate; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const readmeTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,158 @@
|
|
|
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.readmeTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
const handlebars_helpers_1 = require("./handlebars-helpers");
|
|
9
|
+
(0, handlebars_helpers_1.registerHelpers)(handlebars_1.default);
|
|
10
|
+
exports.readmeTemplate = handlebars_1.default.compile(`# Recon Fuzzing Suite
|
|
11
|
+
|
|
12
|
+
This is an auto-generated fuzzing test suite created by [Recon](https://getrecon.xyz/).
|
|
13
|
+
|
|
14
|
+
## File Structure
|
|
15
|
+
|
|
16
|
+
\`\`\`
|
|
17
|
+
{{suiteFolder}}/
|
|
18
|
+
├── Setup.sol # Contract deployment and initialization
|
|
19
|
+
├── BeforeAfter.sol # Ghost variables for state tracking
|
|
20
|
+
├── SelectorStorage.sol # Function selector constants
|
|
21
|
+
├── Properties.sol # YOUR INVARIANTS GO HERE
|
|
22
|
+
├── TargetFunctions.sol # Non-separated target functions
|
|
23
|
+
├── {{cryticTesterName}}.sol # Entry point for Echidna/Medusa
|
|
24
|
+
├── {{cryticToFoundryName}}.sol # Foundry test wrapper
|
|
25
|
+
├── helpers/
|
|
26
|
+
│ └── Utils.sol
|
|
27
|
+
├── managers/
|
|
28
|
+
│ ├── ActorManager.sol
|
|
29
|
+
│ └── AssetManager.sol
|
|
30
|
+
└── targets/
|
|
31
|
+
├── AdminTargets.sol
|
|
32
|
+
└── *Targets.sol # Per-contract targets
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
## Key Concepts
|
|
36
|
+
|
|
37
|
+
### Inheritance Chain
|
|
38
|
+
|
|
39
|
+
\`\`\`
|
|
40
|
+
Setup (deploys contracts)
|
|
41
|
+
└── BeforeAfter (ghost variables, trackOp)
|
|
42
|
+
└── Properties (YOUR invariants)
|
|
43
|
+
└── *Targets (function wrappers)
|
|
44
|
+
└── TargetFunctions
|
|
45
|
+
└── {{cryticTesterName}}
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
### Operation Tracking
|
|
49
|
+
|
|
50
|
+
Each target function uses \`trackOp(selector)\` which:
|
|
51
|
+
1. Sets \`currentOperation\` to the function's selector
|
|
52
|
+
2. Calls \`__before()\` to capture pre-state
|
|
53
|
+
3. Executes the function
|
|
54
|
+
4. Calls \`__after()\` to capture post-state
|
|
55
|
+
|
|
56
|
+
Example property using operation tracking:
|
|
57
|
+
|
|
58
|
+
\`\`\`solidity
|
|
59
|
+
function property_deposit_increases_balance() public {
|
|
60
|
+
if (currentOperation == SelectorStorage.VAULT_DEPOSIT) {
|
|
61
|
+
gte(_after.vaultBalance, _before.vaultBalance, "deposit should increase balance");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
### Ghost Variables
|
|
67
|
+
|
|
68
|
+
In \`BeforeAfter.sol\`, the \`Vars\` struct captures state:
|
|
69
|
+
|
|
70
|
+
\`\`\`solidity
|
|
71
|
+
struct Vars {
|
|
72
|
+
uint256 vaultTotalAssets;
|
|
73
|
+
uint256 userBalance;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function __before() internal {
|
|
77
|
+
_before.vaultTotalAssets = vault.totalAssets();
|
|
78
|
+
_before.userBalance = token.balanceOf(actor);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function __after() internal {
|
|
82
|
+
_after.vaultTotalAssets = vault.totalAssets();
|
|
83
|
+
_after.userBalance = token.balanceOf(actor);
|
|
84
|
+
}
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
### SelectorStorage
|
|
88
|
+
|
|
89
|
+
Contains named constants for function selectors:
|
|
90
|
+
|
|
91
|
+
\`\`\`solidity
|
|
92
|
+
bytes4 constant VAULT_DEPOSIT = bytes4(keccak256("deposit(uint256)"));
|
|
93
|
+
bytes4 constant VAULT_WITHDRAW_0 = bytes4(keccak256("withdraw(uint256)"));
|
|
94
|
+
bytes4 constant VAULT_WITHDRAW_1 = bytes4(keccak256("withdraw(uint256,address)"));
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
Overloaded functions get \_0, \_1 suffixes.
|
|
98
|
+
|
|
99
|
+
## Files to Edit
|
|
100
|
+
|
|
101
|
+
| File | Purpose |
|
|
102
|
+
|------|---------|
|
|
103
|
+
| \`Setup.sol\` | Deploy contracts, set initial state, configure actors |
|
|
104
|
+
| \`BeforeAfter.sol\` | Add variables to \`Vars\` struct, implement \`__before()\`/\`__after()\` |
|
|
105
|
+
| \`Properties.sol\` | Write invariant functions (prefix with \`property_\`) |
|
|
106
|
+
|
|
107
|
+
## Auto-Generated Files (Don't Edit Below Marker)
|
|
108
|
+
|
|
109
|
+
- \`SelectorStorage.sol\` - Regenerated from ABIs
|
|
110
|
+
- \`*Targets.sol\` - Code below \`/// AUTO GENERATED TARGET FUNCTIONS\` marker is regenerated
|
|
111
|
+
|
|
112
|
+
## Writing Properties
|
|
113
|
+
|
|
114
|
+
\`\`\`solidity
|
|
115
|
+
// Global invariant - checked after every operation
|
|
116
|
+
function property_solvent() public {
|
|
117
|
+
gte(vault.totalAssets(), vault.totalSupply(), "insolvent");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Operation-specific invariant
|
|
121
|
+
function property_withdraw_decreases_shares() public {
|
|
122
|
+
if (currentOperation == SelectorStorage.VAULT_WITHDRAW_0) {
|
|
123
|
+
lt(_after.userShares, _before.userShares, "shares should decrease");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
### Assertion Helpers
|
|
129
|
+
|
|
130
|
+
- \`t(bool, string)\` - assert true
|
|
131
|
+
- \`eq(a, b, string)\` - equal
|
|
132
|
+
- \`gt(a, b, string)\` - greater than
|
|
133
|
+
- \`gte(a, b, string)\` - greater or equal
|
|
134
|
+
- \`lt(a, b, string)\` - less than
|
|
135
|
+
- \`lte(a, b, string)\` - less or equal
|
|
136
|
+
|
|
137
|
+
## Running Fuzzers
|
|
138
|
+
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
# Echidna
|
|
141
|
+
echidna . --contract {{cryticTesterName}} --config echidna{{configSuffix}}.yaml
|
|
142
|
+
|
|
143
|
+
# Medusa
|
|
144
|
+
medusa fuzz --config medusa{{configSuffix}}.json
|
|
145
|
+
|
|
146
|
+
# Halmos
|
|
147
|
+
halmos --config halmos{{configSuffix}}.toml
|
|
148
|
+
\`\`\`
|
|
149
|
+
|
|
150
|
+
## Regenerating
|
|
151
|
+
|
|
152
|
+
After contract changes:
|
|
153
|
+
\`\`\`bash
|
|
154
|
+
recon-generate --force
|
|
155
|
+
\`\`\`
|
|
156
|
+
|
|
157
|
+
This preserves Setup.sol, BeforeAfter.sol, Properties.sol, and config files.
|
|
158
|
+
`, { noEscape: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const selectorStorageTemplate: HandlebarsTemplateDelegate<any>;
|
|
@@ -0,0 +1,19 @@
|
|
|
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.selectorStorageTemplate = void 0;
|
|
7
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
|
+
const handlebars_helpers_1 = require("./handlebars-helpers");
|
|
9
|
+
(0, handlebars_helpers_1.registerHelpers)(handlebars_1.default);
|
|
10
|
+
exports.selectorStorageTemplate = handlebars_1.default.compile(`// SPDX-License-Identifier: GPL-2.0
|
|
11
|
+
pragma solidity ^0.8.0;
|
|
12
|
+
|
|
13
|
+
/// @notice Auto-generated selector constants for operation tracking
|
|
14
|
+
library SelectorStorage {
|
|
15
|
+
{{#each functions}}
|
|
16
|
+
bytes4 constant {{selectorConstName this}} = bytes4(keccak256("{{functionSignature this}}"));
|
|
17
|
+
{{/each}}
|
|
18
|
+
}
|
|
19
|
+
`, { noEscape: true });
|
|
@@ -16,6 +16,8 @@ import {vm} from "@chimera/Hevm.sol";
|
|
|
16
16
|
// Helpers
|
|
17
17
|
import {Panic} from "@recon/Panic.sol";
|
|
18
18
|
|
|
19
|
+
import {SelectorStorage} from "./SelectorStorage.sol";
|
|
20
|
+
|
|
19
21
|
// Targets
|
|
20
22
|
// NOTE: Always import and apply them in alphabetical order, so much easier to debug!
|
|
21
23
|
{{#each contracts}}
|
|
@@ -19,6 +19,8 @@ import {vm} from "@chimera/Hevm.sol";
|
|
|
19
19
|
// Helpers
|
|
20
20
|
import {Panic} from "@recon/Panic.sol";
|
|
21
21
|
|
|
22
|
+
import {SelectorStorage} from "../SelectorStorage.sol";
|
|
23
|
+
|
|
22
24
|
{{#if path}}import "{{path}}";{{/if}}
|
|
23
25
|
{{#each extraImports}}
|
|
24
26
|
import { {{this.name}} } from "{{this.path}}";
|
package/dist/types.d.ts
CHANGED
package/dist/types.js
CHANGED