@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,128 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "forge-std/console.sol";
6
+ import "./DiamondForgeHelpers.sol";
7
+
8
+ /**
9
+ * @title DiamondFuzzBase
10
+ * @notice Base contract for Diamond fuzz testing with Forge
11
+ * @dev Provides setup methods and utilities for fuzz testing Diamond contracts
12
+ */
13
+ abstract contract DiamondFuzzBase is Test {
14
+ using DiamondForgeHelpers for address;
15
+
16
+ // Diamond contract address (set by child contracts)
17
+ address public diamond;
18
+
19
+ // Facet addresses (set by child contracts)
20
+ mapping(string => address) public facets;
21
+
22
+ // Test accounts
23
+ address public deployer;
24
+ address public user1;
25
+ address public user2;
26
+ address public user3;
27
+
28
+ /**
29
+ * @notice Setup function called before each test
30
+ * @dev Override this in child contracts to set up Diamond and facets
31
+ */
32
+ function setUp() public virtual {
33
+ // Set up test accounts
34
+ deployer = address(this);
35
+ user1 = makeAddr("user1");
36
+ user2 = makeAddr("user2");
37
+ user3 = makeAddr("user3");
38
+
39
+ // Fund test accounts
40
+ vm.deal(user1, 100 ether);
41
+ vm.deal(user2, 100 ether);
42
+ vm.deal(user3, 100 ether);
43
+
44
+ // Child contracts should override and call setDiamondAddress()
45
+ }
46
+
47
+ /**
48
+ * @notice Set the Diamond contract address
49
+ * @param _diamond Address of the deployed Diamond
50
+ */
51
+ function setDiamondAddress(address _diamond) internal {
52
+ require(_diamond != address(0), "Diamond address cannot be zero");
53
+ diamond = _diamond;
54
+ DiamondForgeHelpers.assertValidDiamond(_diamond);
55
+ }
56
+
57
+ /**
58
+ * @notice Register a facet address
59
+ * @param name Name of the facet
60
+ * @param facetAddress Address of the facet
61
+ */
62
+ function registerFacet(string memory name, address facetAddress) internal {
63
+ require(facetAddress != address(0), "Facet address cannot be zero");
64
+ facets[name] = facetAddress;
65
+ DiamondForgeHelpers.assertValidFacet(facetAddress, name);
66
+ }
67
+
68
+ /**
69
+ * @notice Get a facet address by name
70
+ * @param name Name of the facet
71
+ * @return Address of the facet
72
+ */
73
+ function getFacet(string memory name) internal view returns (address) {
74
+ address facetAddress = facets[name];
75
+ require(facetAddress != address(0), string(abi.encodePacked("Facet not found: ", name)));
76
+ return facetAddress;
77
+ }
78
+
79
+ /**
80
+ * @notice Assume valid Ethereum address for fuzzing
81
+ * @param addr Address to validate
82
+ */
83
+ function assumeValidAddress(address addr) internal pure {
84
+ vm.assume(addr != address(0));
85
+ vm.assume(addr != address(0xdead));
86
+ vm.assume(uint160(addr) > 0xFF); // Avoid precompiles
87
+ }
88
+
89
+ /**
90
+ * @notice Assume valid amount for fuzzing (not zero, not unreasonably large)
91
+ * @param amount Amount to validate
92
+ */
93
+ function assumeValidAmount(uint256 amount) internal pure {
94
+ vm.assume(amount > 0);
95
+ vm.assume(amount < type(uint128).max); // Reasonable upper bound
96
+ }
97
+
98
+ /**
99
+ * @notice Bound a fuzzed value to a specific range
100
+ * @param value The fuzzed value
101
+ * @param min Minimum value (inclusive)
102
+ * @param max Maximum value (inclusive)
103
+ * @return Bounded value
104
+ */
105
+ function boundValue(uint256 value, uint256 min, uint256 max) internal pure returns (uint256) {
106
+ return bound(value, min, max);
107
+ }
108
+
109
+ /**
110
+ * @notice Log test context for debugging
111
+ * @param testName Name of the test
112
+ */
113
+ function logTestContext(string memory testName) internal view {
114
+ console.log("=== Test:", testName, "===");
115
+ console.log("Diamond:", diamond);
116
+ console.log("Deployer:", deployer);
117
+ console.log("Block number:", block.number);
118
+ console.log("Block timestamp:", block.timestamp);
119
+ }
120
+
121
+ /**
122
+ * @notice Expect a revert with specific error message
123
+ * @param errorMessage Expected error message
124
+ */
125
+ function expectRevertWithMessage(string memory errorMessage) internal {
126
+ vm.expectRevert(bytes(errorMessage));
127
+ }
128
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@diamondslab/diamonds-hardhat-foundry",
3
+ "version": "1.0.0",
4
+ "description": "Hardhat plugin that integrates Foundry testing with Diamond proxy contracts, providing deployment helpers and fuzzing support",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/diamondslab/diamonds-hardhat-foundry",
8
+ "directory": "packages/diamonds-hardhat-foundry"
9
+ },
10
+ "homepage": "https://github.com/diamondslab/diamonds-hardhat-foundry",
11
+ "author": "Am0rfu5",
12
+ "license": "MIT",
13
+ "main": "dist/src/index.js",
14
+ "types": "dist/src/index.d.ts",
15
+ "keywords": [
16
+ "ethereum",
17
+ "smart-contracts",
18
+ "hardhat",
19
+ "hardhat-plugin",
20
+ "foundry",
21
+ "forge",
22
+ "diamondslab",
23
+ "diamonds",
24
+ "diamond",
25
+ "solidity",
26
+ "erc-2535",
27
+ "fuzzing",
28
+ "testing",
29
+ "diamond-proxy"
30
+ ],
31
+ "scripts": {
32
+ "lint": "yarn prettier --check && yarn eslint",
33
+ "lint:fix": "yarn prettier --write && yarn eslint --fix",
34
+ "eslint": "eslint 'src/**/*.ts'",
35
+ "prettier": "prettier \"**/*.{js,md,json}\"",
36
+ "pretest": "cd ../.. && yarn build",
37
+ "test": "mocha --recursive \"test/**/*.ts\" --exit",
38
+ "build": "tsc --build .",
39
+ "prepublishOnly": "yarn build",
40
+ "clean": "rimraf dist"
41
+ },
42
+ "files": [
43
+ "dist/src/",
44
+ "src/",
45
+ "contracts/",
46
+ "LICENSE",
47
+ "README.md"
48
+ ],
49
+ "devDependencies": {
50
+ "@types/chai": "^4.2.0",
51
+ "@types/debug": "^4.1.12",
52
+ "@types/mocha": ">=9.1.0",
53
+ "@types/node": "^20.0.0",
54
+ "@typescript-eslint/eslint-plugin": "5.61.0",
55
+ "@typescript-eslint/parser": "5.61.0",
56
+ "chai": "^4.2.0",
57
+ "eslint": "^8.44.0",
58
+ "eslint-config-prettier": "8.3.0",
59
+ "eslint-plugin-import": "2.27.5",
60
+ "eslint-plugin-mocha": "10.4.1",
61
+ "eslint-plugin-prettier": "3.4.0",
62
+ "hardhat": "^2.26.0",
63
+ "mocha": "^10.0.0",
64
+ "prettier": "2.4.1",
65
+ "rimraf": "^3.0.2",
66
+ "ts-node": "^10.8.0",
67
+ "typescript": "~5.0.0"
68
+ },
69
+ "peerDependencies": {
70
+ "@diamondslab/diamonds": "^1.2.1",
71
+ "@diamondslab/hardhat-diamonds": "^1.1.9",
72
+ "hardhat": "^2.26.0"
73
+ },
74
+ "dependencies": {
75
+ "picocolors": "^1.1.0"
76
+ },
77
+ "packageManager": "yarn@4.10.3"
78
+ }
package/src/foundry.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { exec as execCallback, execSync } from "child_process";
2
+ import { NomicLabsHardhatPluginError } from "hardhat/internal/core/errors";
3
+ import picocolors from "picocolors";
4
+ import { promisify } from "util";
5
+
6
+ const exec = promisify(execCallback);
7
+
8
+ type Remappings = Record<string, string>;
9
+
10
+ let cachedRemappings: Promise<Remappings> | undefined;
11
+
12
+ export class HardhatFoundryError extends NomicLabsHardhatPluginError {
13
+ constructor(message: string, parent?: Error) {
14
+ super("diamonds-hardhat-foundry", message, parent);
15
+ }
16
+ }
17
+
18
+ class ForgeInstallError extends HardhatFoundryError {
19
+ constructor(dependency: string, parent: Error) {
20
+ super(
21
+ `Couldn't install '${dependency}', please install it manually.
22
+
23
+ ${parent.message}
24
+ `,
25
+ parent
26
+ );
27
+ }
28
+ }
29
+
30
+ export function getForgeConfig() {
31
+ return JSON.parse(runCmdSync("forge config --json"));
32
+ }
33
+
34
+ export function parseRemappings(remappingsTxt: string): Remappings {
35
+ const remappings: Remappings = {};
36
+ const remappingLines = remappingsTxt.split(/\r\n|\r|\n/);
37
+ for (const remappingLine of remappingLines) {
38
+ if (remappingLine.trim() === "") {
39
+ continue;
40
+ }
41
+
42
+ if (remappingLine.includes(":")) {
43
+ throw new HardhatFoundryError(
44
+ `Invalid remapping '${remappingLine}', remapping contexts are not allowed`
45
+ );
46
+ }
47
+
48
+ if (!remappingLine.includes("=")) {
49
+ throw new HardhatFoundryError(
50
+ `Invalid remapping '${remappingLine}', remappings without a target are not allowed`
51
+ );
52
+ }
53
+
54
+ const fromTo = remappingLine.split("=");
55
+
56
+ // if the remapping already exists, we ignore it because the first one wins
57
+ if (remappings[fromTo[0]] !== undefined) {
58
+ continue;
59
+ }
60
+
61
+ remappings[fromTo[0]] = fromTo[1];
62
+ }
63
+
64
+ return remappings;
65
+ }
66
+
67
+ export async function getRemappings() {
68
+ // Get remappings only once
69
+ if (cachedRemappings === undefined) {
70
+ cachedRemappings = runCmd("forge remappings").then(parseRemappings);
71
+ }
72
+
73
+ return cachedRemappings;
74
+ }
75
+
76
+ export async function installDependency(dependency: string) {
77
+ // Check if --no-commit flag is supported. Best way is checking the help text
78
+ const helpText = await runCmd("forge install --help");
79
+ const useNoCommitFlag = helpText.includes("--no-commit");
80
+
81
+ const cmd = `forge install ${
82
+ useNoCommitFlag ? "--no-commit" : ""
83
+ } ${dependency}`;
84
+
85
+ console.log(`Running '${picocolors.blue(cmd)}'`);
86
+
87
+ try {
88
+ await exec(cmd);
89
+ } catch (error: any) {
90
+ throw new ForgeInstallError(dependency, error);
91
+ }
92
+ }
93
+
94
+ function runCmdSync(cmd: string): string {
95
+ try {
96
+ return execSync(cmd, { stdio: "pipe" }).toString();
97
+ } catch (error: any) {
98
+ const pluginError = buildForgeExecutionError(
99
+ error.status,
100
+ error.stderr.toString()
101
+ );
102
+
103
+ throw pluginError;
104
+ }
105
+ }
106
+
107
+ async function runCmd(cmd: string): Promise<string> {
108
+ try {
109
+ const { stdout } = await exec(cmd);
110
+ return stdout;
111
+ } catch (error: any) {
112
+ throw buildForgeExecutionError(error.code, error.message);
113
+ }
114
+ }
115
+
116
+ function buildForgeExecutionError(
117
+ exitCode: number | undefined,
118
+ message: string
119
+ ) {
120
+ switch (exitCode) {
121
+ case 127:
122
+ return new HardhatFoundryError(
123
+ "Couldn't run `forge`. Please check that your foundry installation is correct."
124
+ );
125
+ case 134:
126
+ return new HardhatFoundryError(
127
+ "Running `forge` failed. Please check that your foundry.toml file is correct."
128
+ );
129
+ default:
130
+ return new HardhatFoundryError(
131
+ `Unexpected error while running \`forge\`: ${message}`
132
+ );
133
+ }
134
+ }
@@ -0,0 +1,210 @@
1
+ import { Diamond } from "@diamondslab/diamonds";
2
+ import { existsSync } from "fs";
3
+ import { HardhatRuntimeEnvironment } from "hardhat/types";
4
+ import { join } from "path";
5
+ import { Logger } from "../utils/logger";
6
+
7
+ // Type for LocalDiamondDeployer (avoiding import issues)
8
+ type LocalDiamondDeployerConfig = {
9
+ diamondName: string;
10
+ networkName: string;
11
+ provider: any;
12
+ chainId: number;
13
+ writeDeployedDiamondData: boolean;
14
+ };
15
+
16
+ /**
17
+ * DeploymentManager - Manages Diamond deployment lifecycle for Forge testing
18
+ *
19
+ * Note: This class dynamically requires LocalDiamondDeployer from the workspace
20
+ * to avoid module resolution issues in the published package.
21
+ */
22
+ export class DeploymentManager {
23
+ constructor(private hre: HardhatRuntimeEnvironment) {}
24
+
25
+ /**
26
+ * Get LocalDiamondDeployer class
27
+ * @private
28
+ */
29
+ private async getDeployerClass(): Promise<any> {
30
+ // LocalDiamondDeployer is in the workspace scripts, not exported from the module
31
+ // This will need to be available in user projects
32
+ const localDeployerPath = join(
33
+ this.hre.config.paths.root,
34
+ "scripts/setup/LocalDiamondDeployer"
35
+ );
36
+
37
+ try {
38
+ const deployer = await import(localDeployerPath);
39
+ return deployer.LocalDiamondDeployer;
40
+ } catch {
41
+ throw new Error(
42
+ "LocalDiamondDeployer not found. Make sure your project has scripts/setup/LocalDiamondDeployer.ts"
43
+ );
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Deploy a Diamond using LocalDiamondDeployer
49
+ * @param diamondName - Name of the Diamond to deploy
50
+ * @param networkName - Target network (hardhat, localhost, anvil)
51
+ * @param force - Force redeployment even if exists
52
+ */
53
+ async deploy(
54
+ diamondName: string = "ExampleDiamond",
55
+ networkName: string = "hardhat",
56
+ force: boolean = false
57
+ ): Promise<Diamond> {
58
+ Logger.section(`Deploying ${diamondName} to ${networkName}`);
59
+
60
+ // Get provider and network info
61
+ const provider = this.hre.ethers.provider;
62
+ const network = await provider.getNetwork();
63
+ const chainId = Number(network.chainId);
64
+
65
+ // Check if deployment exists and handle force flag
66
+ if (!force && this.hasDeploymentRecord(diamondName, networkName, chainId)) {
67
+ Logger.info("Deployment record exists, using existing deployment");
68
+ Logger.info("Use --force to redeploy");
69
+
70
+ const LocalDiamondDeployer = await this.getDeployerClass();
71
+ const deployer = await LocalDiamondDeployer.getInstance({
72
+ diamondName,
73
+ networkName,
74
+ provider,
75
+ chainId,
76
+ writeDeployedDiamondData: true,
77
+ } as LocalDiamondDeployerConfig);
78
+
79
+ return await deployer.getDiamond();
80
+ }
81
+
82
+ Logger.step("Initializing LocalDiamondDeployer...");
83
+
84
+ const LocalDiamondDeployer = await this.getDeployerClass();
85
+
86
+ const deployer = await LocalDiamondDeployer.getInstance({
87
+ diamondName,
88
+ networkName,
89
+ provider,
90
+ chainId,
91
+ writeDeployedDiamondData: true,
92
+ } as LocalDiamondDeployerConfig);
93
+
94
+ await deployer.setVerbose(false); // Reduce noise, use our logger instead
95
+
96
+ Logger.step("Deploying Diamond contract...");
97
+ const diamond = await deployer.getDiamondDeployed();
98
+
99
+ const deployedData = diamond.getDeployedDiamondData();
100
+
101
+ Logger.success(`Diamond deployed at: ${deployedData.DiamondAddress}`);
102
+ Logger.info(`Deployer: ${deployedData.DeployerAddress}`);
103
+ Logger.info(`Facets deployed: ${Object.keys(deployedData.DeployedFacets || {}).length}`);
104
+
105
+ return diamond;
106
+ }
107
+
108
+ /**
109
+ * Get existing deployment record
110
+ * @param diamondName - Name of the Diamond
111
+ * @param networkName - Network name
112
+ * @param chainId - Chain ID
113
+ */
114
+ async getDeployment(
115
+ diamondName: string = "ExampleDiamond",
116
+ networkName: string = "hardhat",
117
+ chainId?: number
118
+ ): Promise<Diamond | null> {
119
+ try {
120
+ const provider = this.hre.ethers.provider;
121
+ const network = await provider.getNetwork();
122
+ const actualChainId = chainId ?? Number(network.chainId);
123
+
124
+ if (!this.hasDeploymentRecord(diamondName, networkName, actualChainId)) {
125
+ Logger.warn("No deployment record found");
126
+ return null;
127
+ }
128
+
129
+ const LocalDiamondDeployer = await this.getDeployerClass();
130
+ const deployer = await LocalDiamondDeployer.getInstance({
131
+ diamondName,
132
+ networkName,
133
+ provider,
134
+ chainId: actualChainId,
135
+ writeDeployedDiamondData: false,
136
+ } as LocalDiamondDeployerConfig);
137
+
138
+ return await deployer.getDiamond();
139
+ } catch (error) {
140
+ Logger.error(`Failed to get deployment: ${error}`);
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Ensure deployment exists, deploy if needed
147
+ * @param diamondName - Name of the Diamond
148
+ * @param networkName - Network name
149
+ * @param force - Force redeployment
150
+ */
151
+ async ensureDeployment(
152
+ diamondName: string = "ExampleDiamond",
153
+ networkName: string = "hardhat",
154
+ force: boolean = false
155
+ ): Promise<Diamond> {
156
+ const provider = this.hre.ethers.provider;
157
+ const network = await provider.getNetwork();
158
+ const chainId = Number(network.chainId);
159
+
160
+ // Check if deployment exists
161
+ const existing = await this.getDeployment(diamondName, networkName, chainId);
162
+
163
+ if (existing && !force) {
164
+ Logger.info("Using existing deployment");
165
+ return existing;
166
+ }
167
+
168
+ // Deploy if not exists or force is true
169
+ return await this.deploy(diamondName, networkName, force);
170
+ }
171
+
172
+ /**
173
+ * Check if deployment record exists
174
+ * @private
175
+ */
176
+ private hasDeploymentRecord(
177
+ diamondName: string,
178
+ networkName: string,
179
+ chainId: number
180
+ ): boolean {
181
+ const deploymentFileName = `${diamondName.toLowerCase()}-${networkName.toLowerCase()}-${chainId}.json`;
182
+ const deploymentPath = join(
183
+ this.hre.config.paths.root,
184
+ "diamonds",
185
+ diamondName,
186
+ "deployments",
187
+ deploymentFileName
188
+ );
189
+
190
+ return existsSync(deploymentPath);
191
+ }
192
+
193
+ /**
194
+ * Get deployment file path
195
+ */
196
+ getDeploymentPath(
197
+ diamondName: string,
198
+ networkName: string,
199
+ chainId: number
200
+ ): string {
201
+ const deploymentFileName = `${diamondName.toLowerCase()}-${networkName.toLowerCase()}-${chainId}.json`;
202
+ return join(
203
+ this.hre.config.paths.root,
204
+ "diamonds",
205
+ diamondName,
206
+ "deployments",
207
+ deploymentFileName
208
+ );
209
+ }
210
+ }