@diamondslab/diamonds-hardhat-foundry 1.0.0

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.
@@ -0,0 +1,194 @@
1
+ import { HardhatRuntimeEnvironment } from "hardhat/types";
2
+ import { compileForge, isFoundryInstalled, runForgeTest } from "../utils/foundry";
3
+ import { Logger } from "../utils/logger";
4
+ import { DeploymentManager } from "./DeploymentManager";
5
+ import { HelperGenerator } from "./HelperGenerator";
6
+
7
+ export interface ForgeTestOptions {
8
+ /** Name of the Diamond to deploy */
9
+ diamondName?: string;
10
+ /** Network to deploy on */
11
+ networkName?: string;
12
+ /** Force redeployment */
13
+ force?: boolean;
14
+ /** Match test pattern */
15
+ matchTest?: string;
16
+ /** Match contract pattern */
17
+ matchContract?: string;
18
+ /** Verbosity level (1-5) */
19
+ verbosity?: number;
20
+ /** Show gas report */
21
+ gasReport?: boolean;
22
+ /** Skip helper generation */
23
+ skipHelpers?: boolean;
24
+ /** Skip deployment (use existing) */
25
+ skipDeployment?: boolean;
26
+ }
27
+
28
+ /**
29
+ * ForgeFuzzingFramework - Main orchestration class for Forge testing with Diamonds
30
+ *
31
+ * Coordinates:
32
+ * 1. Diamond deployment via DeploymentManager
33
+ * 2. Helper generation via HelperGenerator
34
+ * 3. Forge test execution
35
+ */
36
+ export class ForgeFuzzingFramework {
37
+ private deploymentManager: DeploymentManager;
38
+ private helperGenerator: HelperGenerator;
39
+
40
+ constructor(private hre: HardhatRuntimeEnvironment) {
41
+ this.deploymentManager = new DeploymentManager(hre);
42
+ this.helperGenerator = new HelperGenerator(hre);
43
+ }
44
+
45
+ /**
46
+ * Run complete Forge testing workflow
47
+ *
48
+ * Workflow:
49
+ * 1. Validate Foundry installation
50
+ * 2. Deploy or reuse Diamond
51
+ * 3. Generate Solidity helpers
52
+ * 4. Compile Forge contracts
53
+ * 5. Run Forge tests
54
+ *
55
+ * @param options - Test execution options
56
+ */
57
+ async runTests(options: ForgeTestOptions = {}): Promise<boolean> {
58
+ const {
59
+ diamondName = "ExampleDiamond",
60
+ networkName = "hardhat",
61
+ force = false,
62
+ matchTest,
63
+ matchContract,
64
+ verbosity = 2,
65
+ gasReport = false,
66
+ skipHelpers = false,
67
+ skipDeployment = false,
68
+ } = options;
69
+
70
+ Logger.section("Forge Fuzzing Framework - Test Execution");
71
+
72
+ // Step 1: Validate Foundry
73
+ if (!isFoundryInstalled()) {
74
+ Logger.error("Foundry is not installed. Please install it: https://book.getfoundry.sh/getting-started/installation");
75
+ return false;
76
+ }
77
+
78
+ try {
79
+ // Step 2: Ensure Diamond deployment
80
+ if (!skipDeployment) {
81
+ Logger.section("Step 1/4: Ensuring Diamond Deployment");
82
+ await this.deploymentManager.ensureDeployment(
83
+ diamondName,
84
+ networkName,
85
+ force
86
+ );
87
+ } else {
88
+ Logger.info("Skipping deployment (using existing)");
89
+ }
90
+
91
+ // Step 3: Generate helpers
92
+ if (!skipHelpers) {
93
+ Logger.section("Step 2/4: Generating Solidity Helpers");
94
+ const deployment = await this.deploymentManager.getDeployment(
95
+ diamondName,
96
+ networkName
97
+ );
98
+
99
+ if (!deployment) {
100
+ Logger.error("No deployment found. Cannot generate helpers.");
101
+ return false;
102
+ }
103
+
104
+ const provider = this.hre.ethers.provider;
105
+ const network = await provider.getNetwork();
106
+ const chainId = Number(network.chainId);
107
+
108
+ const deploymentData = deployment.getDeployedDiamondData();
109
+
110
+ await this.helperGenerator.generateDeploymentHelpers(
111
+ diamondName,
112
+ networkName,
113
+ chainId,
114
+ deploymentData
115
+ );
116
+ } else {
117
+ Logger.info("Skipping helper generation");
118
+ }
119
+
120
+ // Step 4: Compile Forge contracts
121
+ Logger.section("Step 3/4: Compiling Forge Contracts");
122
+ const compileResult = await compileForge({
123
+ cwd: this.hre.config.paths.root,
124
+ verbose: verbosity >= 3,
125
+ });
126
+
127
+ if (!compileResult.success) {
128
+ Logger.error("Forge compilation failed");
129
+ return false;
130
+ }
131
+
132
+ // Step 5: Run tests
133
+ Logger.section("Step 4/4: Running Forge Tests");
134
+ const testResult = await runForgeTest({
135
+ matchTest,
136
+ matchContract,
137
+ verbosity,
138
+ gasReport,
139
+ cwd: this.hre.config.paths.root,
140
+ });
141
+
142
+ return testResult.success;
143
+ } catch (error: any) {
144
+ Logger.error(`Test execution failed: ${error.message}`);
145
+ return false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Deploy Diamond only (no testing)
151
+ */
152
+ async deployOnly(
153
+ diamondName: string = "ExampleDiamond",
154
+ networkName: string = "hardhat",
155
+ force: boolean = false
156
+ ) {
157
+ return await this.deploymentManager.ensureDeployment(
158
+ diamondName,
159
+ networkName,
160
+ force
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Generate helpers only (no deployment or testing)
166
+ */
167
+ async generateHelpersOnly(
168
+ diamondName: string = "ExampleDiamond",
169
+ networkName: string = "hardhat"
170
+ ) {
171
+ const deployment = await this.deploymentManager.getDeployment(
172
+ diamondName,
173
+ networkName
174
+ );
175
+
176
+ if (!deployment) {
177
+ throw new Error("No deployment found. Deploy first using deployOnly()");
178
+ }
179
+
180
+ const provider = this.hre.ethers.provider;
181
+ const network = await provider.getNetwork();
182
+ const chainId = Number(network.chainId);
183
+
184
+ const deploymentData = deployment.getDeployedDiamondData();
185
+
186
+ return await this.helperGenerator.generateDeploymentHelpers(
187
+ diamondName,
188
+ networkName,
189
+ chainId,
190
+ deploymentData
191
+ );
192
+ }
193
+ }
194
+
@@ -0,0 +1,246 @@
1
+ import { DeployedDiamondData } from "@diamondslab/diamonds";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { HardhatRuntimeEnvironment } from "hardhat/types";
4
+ import { join } from "path";
5
+ import { Logger } from "../utils/logger";
6
+
7
+ /**
8
+ * HelperGenerator - Generates Solidity helper files for testing
9
+ */
10
+ export class HelperGenerator {
11
+ constructor(private hre: HardhatRuntimeEnvironment) {}
12
+
13
+ /**
14
+ * Scaffold project with initial test structure
15
+ */
16
+ async scaffoldProject(outputDir?: string): Promise<void> {
17
+ const helpersDir = outputDir || this.hre.diamondsFoundry.helpersDir;
18
+ const basePath = join(this.hre.config.paths.root, helpersDir);
19
+
20
+ Logger.section("Scaffolding Forge Test Structure");
21
+
22
+ // Create directories
23
+ Logger.step("Creating directories...");
24
+ mkdirSync(basePath, { recursive: true });
25
+ mkdirSync(join(basePath, "../unit"), { recursive: true });
26
+ mkdirSync(join(basePath, "../integration"), { recursive: true });
27
+ mkdirSync(join(basePath, "../fuzz"), { recursive: true });
28
+
29
+ Logger.success(`Test structure created at ${basePath}`);
30
+ }
31
+
32
+ /**
33
+ * Generate DiamondDeployment.sol from deployment record
34
+ */
35
+ async generateDeploymentHelpers(
36
+ diamondName: string,
37
+ networkName: string,
38
+ chainId: number,
39
+ deploymentData: DeployedDiamondData
40
+ ): Promise<string> {
41
+ Logger.section("Generating Diamond Deployment Helper");
42
+
43
+ const helpersDir = this.hre.diamondsFoundry.helpersDir;
44
+ const outputPath = join(
45
+ this.hre.config.paths.root,
46
+ helpersDir,
47
+ "DiamondDeployment.sol"
48
+ );
49
+
50
+ const content = this.generateLibrarySource(
51
+ diamondName,
52
+ networkName,
53
+ chainId,
54
+ deploymentData
55
+ );
56
+
57
+ // Ensure directory exists
58
+ mkdirSync(join(this.hre.config.paths.root, helpersDir), {
59
+ recursive: true,
60
+ });
61
+
62
+ // Write file
63
+ writeFileSync(outputPath, content, "utf8");
64
+
65
+ Logger.success(`Generated: ${outputPath}`);
66
+ return outputPath;
67
+ }
68
+
69
+ /**
70
+ * Generate example test files
71
+ */
72
+ async generateExampleTests(): Promise<string[]> {
73
+ const generated: string[] = [];
74
+ const examples = this.hre.diamondsFoundry.exampleTests;
75
+
76
+ if (!this.hre.diamondsFoundry.generateExamples) {
77
+ Logger.info("Example generation disabled in config");
78
+ return generated;
79
+ }
80
+
81
+ Logger.section("Generating Example Tests");
82
+
83
+ const basePath = join(this.hre.config.paths.root, "test", "foundry");
84
+ const templatesPath = join(__dirname, "../templates");
85
+
86
+ for (const type of examples) {
87
+ let templateFile = "";
88
+ let outputPath = "";
89
+
90
+ switch (type) {
91
+ case "unit":
92
+ templateFile = join(templatesPath, "ExampleUnitTest.t.sol.template");
93
+ outputPath = join(basePath, "unit", "ExampleUnit.t.sol");
94
+ break;
95
+ case "integration":
96
+ templateFile = join(templatesPath, "ExampleIntegrationTest.t.sol.template");
97
+ outputPath = join(basePath, "integration", "ExampleIntegration.t.sol");
98
+ break;
99
+ case "fuzz":
100
+ templateFile = join(templatesPath, "ExampleFuzzTest.t.sol.template");
101
+ outputPath = join(basePath, "fuzz", "ExampleFuzz.t.sol");
102
+ break;
103
+ default:
104
+ Logger.warn(`Unknown example type: ${type}`);
105
+ continue;
106
+ }
107
+
108
+ try {
109
+ // Check if template exists
110
+ if (!existsSync(templateFile)) {
111
+ Logger.warn(`Template not found: ${templateFile}`);
112
+ continue;
113
+ }
114
+
115
+ // Read template content
116
+ const templateContent = readFileSync(templateFile, "utf8");
117
+
118
+ // Ensure output directory exists
119
+ mkdirSync(join(basePath, type), { recursive: true });
120
+
121
+ // Check if file already exists
122
+ if (existsSync(outputPath)) {
123
+ Logger.info(`Skipping ${type} example (already exists): ${outputPath}`);
124
+ continue;
125
+ }
126
+
127
+ // Write example test file
128
+ writeFileSync(outputPath, templateContent, "utf8");
129
+ Logger.success(`Generated ${type} example: ${outputPath}`);
130
+ generated.push(outputPath);
131
+ } catch (error: any) {
132
+ Logger.error(`Failed to generate ${type} example: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ if (generated.length === 0) {
137
+ Logger.info("No new example tests generated (may already exist)");
138
+ }
139
+
140
+ return generated;
141
+ }
142
+
143
+ /**
144
+ * Generate Solidity library source from deployment data
145
+ * @private
146
+ */
147
+ private generateLibrarySource(
148
+ diamondName: string,
149
+ networkName: string,
150
+ chainId: number,
151
+ deploymentData: DeployedDiamondData
152
+ ): string {
153
+ const timestamp = new Date().toISOString();
154
+ const networkInfo = `${networkName}-${chainId}`;
155
+ const deploymentFileName = `${diamondName.toLowerCase()}-${networkInfo}.json`;
156
+ const deploymentFilePath = `diamonds/${diamondName}/deployments/${deploymentFileName}`;
157
+
158
+ let source = "";
159
+
160
+ // SPDX and pragma
161
+ source += "// SPDX-License-Identifier: MIT\n";
162
+ source += "pragma solidity ^0.8.19;\n\n";
163
+
164
+ // Header comments
165
+ source += "/**\n";
166
+ source += ` * @title DiamondDeployment\n`;
167
+ source += ` * @notice Auto-generated deployment data for ${diamondName}\n`;
168
+ source += ` * @dev This library provides constants and helper functions for accessing\n`;
169
+ source += ` * deployment data in Forge tests. It is auto-generated from the deployment\n`;
170
+ source += ` * record and should not be edited manually.\n`;
171
+ source += ` *\n`;
172
+ source += ` * Generated from: ${deploymentFilePath}\n`;
173
+ source += ` * Generated at: ${timestamp}\n`;
174
+ source += ` *\n`;
175
+ source += ` * To regenerate this file:\n`;
176
+ source += ` * npx hardhat diamonds-forge:generate-helpers --diamond ${diamondName}\n`;
177
+ source += ` *\n`;
178
+ source += ` * ⚠️ DO NOT EDIT MANUALLY - Changes will be overwritten on next generation\n`;
179
+ source += " */\n";
180
+ source += "library DiamondDeployment {\n";
181
+
182
+ // Diamond address
183
+ source += ` /// @notice Address of the deployed ${diamondName} contract\n`;
184
+ source += ` /// @dev This is the main Diamond proxy address\n`;
185
+ source += ` address constant DIAMOND_ADDRESS = ${deploymentData.DiamondAddress};\n\n`;
186
+
187
+ // Facet addresses
188
+ source += " // ========================================\n";
189
+ source += " // Facet Addresses\n";
190
+ source += " // ========================================\n\n";
191
+
192
+ const facets = deploymentData.DeployedFacets ?? {};
193
+ for (const [facetName, facetData] of Object.entries(facets)) {
194
+ const constantName = facetName
195
+ .replace(/Facet$/, "")
196
+ .replace(/([A-Z])/g, "_$1")
197
+ .toUpperCase()
198
+ .replace(/^_/, "") + "_FACET";
199
+
200
+ source += ` /// @notice Address of ${facetName} implementation\n`;
201
+ source += ` address constant ${constantName} = ${facetData.address};\n`;
202
+ }
203
+ source += "\n";
204
+
205
+ // Helper functions
206
+ source += " // ========================================\n";
207
+ source += " // Helper Functions\n";
208
+ source += " // ========================================\n\n";
209
+
210
+ source += " /**\n";
211
+ source += " * @notice Get the Diamond contract address\n";
212
+ source += " * @return The address of the deployed Diamond proxy\n";
213
+ source += " */\n";
214
+ source += " function getDiamondAddress() internal pure returns (address) {\n";
215
+ source += " return DIAMOND_ADDRESS;\n";
216
+ source += " }\n\n";
217
+
218
+ source += " /**\n";
219
+ source += " * @notice Get facet implementation address by name\n";
220
+ source += " * @param facetName The name of the facet\n";
221
+ source += " * @return The address of the facet implementation\n";
222
+ source += " */\n";
223
+ source += " function getFacetAddress(string memory facetName) internal pure returns (address) {\n";
224
+
225
+ let firstFacet = true;
226
+ for (const [facetName, facetData] of Object.entries(facets)) {
227
+ const constantName = facetName
228
+ .replace(/Facet$/, "")
229
+ .replace(/([A-Z])/g, "_$1")
230
+ .toUpperCase()
231
+ .replace(/^_/, "") + "_FACET";
232
+
233
+ const condition = firstFacet ? "if" : "else if";
234
+ source += ` ${condition} (keccak256(bytes(facetName)) == keccak256(bytes("${facetName}"))) {\n`;
235
+ source += ` return ${constantName};\n`;
236
+ source += " }\n";
237
+ firstFacet = false;
238
+ }
239
+ source += " return address(0);\n";
240
+ source += " }\n";
241
+
242
+ source += "}\n";
243
+
244
+ return source;
245
+ }
246
+ }
package/src/index.ts ADDED
@@ -0,0 +1,166 @@
1
+ import { extendConfig, extendEnvironment, internalTask, task } from "hardhat/config";
2
+ import "./tasks/deploy";
3
+ import "./tasks/generate-helpers";
4
+ import "./tasks/init";
5
+ import "./tasks/test";
6
+ import "./types/hardhat";
7
+
8
+ import { existsSync, writeFileSync } from "fs";
9
+ import {
10
+ TASK_COMPILE_GET_REMAPPINGS,
11
+ TASK_COMPILE_TRANSFORM_IMPORT_NAME,
12
+ } from "hardhat/builtin-tasks/task-names";
13
+ import { HardhatRuntimeEnvironment } from "hardhat/types";
14
+ import path from "path";
15
+ import picocolors from "picocolors";
16
+ import {
17
+ getForgeConfig,
18
+ getRemappings,
19
+ HardhatFoundryError,
20
+ installDependency,
21
+ } from "./foundry";
22
+ import { validateConfig } from "./utils/validation";
23
+
24
+ // Export framework classes for programmatic use
25
+ export { DeploymentManager } from "./framework/DeploymentManager";
26
+ export { ForgeFuzzingFramework } from "./framework/ForgeFuzzingFramework";
27
+ export { HelperGenerator } from "./framework/HelperGenerator";
28
+
29
+ // Export types
30
+ export * from "./types/config";
31
+
32
+ const TASK_INIT_FOUNDRY = "init-foundry";
33
+
34
+ let pluginActivated = false;
35
+
36
+ // Extend config with diamondsFoundry settings
37
+ extendConfig((config, userConfig) => {
38
+ // Validate and set diamondsFoundry config
39
+ config.diamondsFoundry = validateConfig(userConfig.diamondsFoundry);
40
+
41
+ // Check foundry.toml presence. Don't warn when running foundry initialization task
42
+ if (!existsSync(path.join(config.paths.root, "foundry.toml"))) {
43
+ if (!process.argv.includes(TASK_INIT_FOUNDRY)) {
44
+ console.log(
45
+ picocolors.yellow(
46
+ `Warning: You are using the diamonds-hardhat-foundry plugin but there isn't a foundry.toml file in your project. Run 'npx hardhat ${TASK_INIT_FOUNDRY}' to create one.`
47
+ )
48
+ );
49
+ }
50
+ return;
51
+ }
52
+
53
+ // Load foundry config
54
+ const foundryConfig = getForgeConfig();
55
+
56
+ // Ensure required keys exist
57
+ if (
58
+ foundryConfig?.src === undefined ||
59
+ foundryConfig?.cache_path === undefined
60
+ ) {
61
+ throw new HardhatFoundryError(
62
+ "Couldn't find `src` or `cache_path` config keys after running `forge config --json`"
63
+ );
64
+ }
65
+
66
+ // Ensure foundry src path doesn't mismatch user-configured path
67
+ const userSourcesPath = userConfig.paths?.sources;
68
+ const foundrySourcesPath = foundryConfig.src;
69
+
70
+ if (
71
+ userSourcesPath !== undefined &&
72
+ path.resolve(userSourcesPath) !== path.resolve(foundrySourcesPath)
73
+ ) {
74
+ throw new HardhatFoundryError(
75
+ `User-configured sources path (${userSourcesPath}) doesn't match path configured in foundry (${foundrySourcesPath})`
76
+ );
77
+ }
78
+
79
+ // Set sources path
80
+ config.paths.sources = path.resolve(config.paths.root, foundrySourcesPath);
81
+
82
+ // Change hardhat's cache path if it clashes with foundry's
83
+ const foundryCachePath = path.resolve(
84
+ config.paths.root,
85
+ foundryConfig.cache_path
86
+ );
87
+ if (config.paths.cache === foundryCachePath) {
88
+ config.paths.cache = "cache_hardhat";
89
+ }
90
+
91
+ pluginActivated = true;
92
+ });
93
+
94
+ // Extend environment to add diamondsFoundry config to HRE
95
+ extendEnvironment((hre: HardhatRuntimeEnvironment) => {
96
+ hre.diamondsFoundry = hre.config.diamondsFoundry;
97
+ });
98
+
99
+ // This task is in place to detect old hardhat-core versions
100
+ internalTask(TASK_COMPILE_TRANSFORM_IMPORT_NAME).setAction(
101
+ async (
102
+ {
103
+ importName,
104
+ deprecationCheck,
105
+ }: { importName: string; deprecationCheck: boolean },
106
+ _hre
107
+ ): Promise<string> => {
108
+ // When the deprecationCheck param is passed, it means a new enough hardhat-core is being used
109
+ if (deprecationCheck) {
110
+ return importName;
111
+ }
112
+ throw new HardhatFoundryError(
113
+ "This version of diamonds-hardhat-foundry depends on hardhat version >= 2.17.2"
114
+ );
115
+ }
116
+ );
117
+
118
+ internalTask(TASK_COMPILE_GET_REMAPPINGS).setAction(
119
+ async (): Promise<Record<string, string>> => {
120
+ if (!pluginActivated) {
121
+ return {};
122
+ }
123
+
124
+ return getRemappings();
125
+ }
126
+ );
127
+
128
+ task(
129
+ TASK_INIT_FOUNDRY,
130
+ "Initialize foundry setup in current hardhat project",
131
+ async (_, hre: HardhatRuntimeEnvironment) => {
132
+ const foundryConfigPath = path.resolve(
133
+ hre.config.paths.root,
134
+ "foundry.toml"
135
+ );
136
+
137
+ if (existsSync(foundryConfigPath)) {
138
+ console.warn(
139
+ picocolors.yellow(`File foundry.toml already exists. Aborting.`)
140
+ );
141
+ process.exit(1);
142
+ }
143
+
144
+ console.log(`Creating foundry.toml file...`);
145
+
146
+ writeFileSync(
147
+ foundryConfigPath,
148
+ [
149
+ `[profile.default]`,
150
+ `src = '${path.relative(
151
+ hre.config.paths.root,
152
+ hre.config.paths.sources
153
+ )}'`,
154
+ `out = 'out'`,
155
+ `libs = ['node_modules', 'lib']`,
156
+ `test = '${path.relative(
157
+ hre.config.paths.root,
158
+ hre.config.paths.tests
159
+ )}'`,
160
+ `cache_path = 'cache_forge'`,
161
+ ].join("\n")
162
+ );
163
+
164
+ await installDependency("foundry-rs/forge-std");
165
+ }
166
+ );