@diamondslab/diamonds-hardhat-foundry 1.0.3 → 2.2.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.
- package/CHANGELOG.md +249 -0
- package/README.md +650 -4
- package/contracts/DiamondABILoader.sol +329 -0
- package/contracts/DiamondForgeHelpers.sol +309 -85
- package/contracts/DiamondFuzzBase.sol +322 -114
- package/dist/foundry.d.ts +28 -0
- package/dist/foundry.d.ts.map +1 -1
- package/dist/foundry.js +82 -1
- package/dist/foundry.js.map +1 -1
- package/dist/framework/DeploymentManager.d.ts +10 -11
- package/dist/framework/DeploymentManager.d.ts.map +1 -1
- package/dist/framework/DeploymentManager.js +56 -45
- package/dist/framework/DeploymentManager.js.map +1 -1
- package/dist/framework/ForgeFuzzingFramework.d.ts +4 -0
- package/dist/framework/ForgeFuzzingFramework.d.ts.map +1 -1
- package/dist/framework/ForgeFuzzingFramework.js +16 -2
- package/dist/framework/ForgeFuzzingFramework.js.map +1 -1
- package/dist/framework/HelperGenerator.d.ts.map +1 -1
- package/dist/framework/HelperGenerator.js +11 -0
- package/dist/framework/HelperGenerator.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +1 -1
- package/dist/tasks/deploy.js +11 -4
- package/dist/tasks/deploy.js.map +1 -1
- package/dist/tasks/generate-helpers.js +6 -4
- package/dist/tasks/generate-helpers.js.map +1 -1
- package/dist/tasks/init.js +3 -2
- package/dist/tasks/init.js.map +1 -1
- package/dist/tasks/test.js +13 -2
- package/dist/tasks/test.js.map +1 -1
- package/dist/types/hardhat.d.ts +1 -1
- package/dist/types/hardhat.d.ts.map +1 -1
- package/dist/types/hardhat.js +1 -0
- package/dist/types/hardhat.js.map +1 -1
- package/dist/utils/foundry.d.ts +1 -0
- package/dist/utils/foundry.d.ts.map +1 -1
- package/dist/utils/foundry.js +3 -0
- package/dist/utils/foundry.js.map +1 -1
- package/package.json +5 -3
- package/src/foundry.ts +104 -0
- package/src/framework/DeploymentManager.ts +74 -69
- package/src/framework/ForgeFuzzingFramework.ts +23 -1
- package/src/framework/HelperGenerator.ts +13 -0
- package/src/index.ts +0 -5
- package/src/tasks/deploy.ts +14 -3
- package/src/tasks/generate-helpers.ts +6 -2
- package/src/tasks/init.ts +3 -1
- package/src/tasks/test.ts +12 -1
- package/src/templates/ExampleFuzzTest.t.sol.template +26 -17
- package/src/templates/ExampleIntegrationTest.t.sol.template +9 -1
- package/src/templates/ExampleUnitTest.t.sol.template +7 -1
- package/src/types/hardhat.ts +1 -1
- package/src/utils/foundry.ts +5 -0
- package/dist/templates/DiamondDeployment.sol.template +0 -38
- package/dist/templates/ExampleFuzzTest.t.sol.template +0 -109
- package/dist/templates/ExampleIntegrationTest.t.sol.template +0 -79
- package/dist/templates/ExampleUnitTest.t.sol.template +0 -59
|
@@ -2,95 +2,319 @@
|
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
4
|
import "forge-std/Test.sol";
|
|
5
|
-
import "forge-std/
|
|
5
|
+
import "forge-std/Vm.sol";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/// @title DiamondForgeHelpers
|
|
8
|
+
/// @author DiamondsLab
|
|
9
|
+
/// @notice Utility library for Diamond testing with Forge
|
|
10
|
+
/// @dev Provides assertion helpers, address validation, and common test utilities for Diamond contracts
|
|
11
|
+
/// @custom:security This library is intended for testing purposes only
|
|
12
12
|
library DiamondForgeHelpers {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
13
|
+
/// @notice Forge VM interface for cheat codes
|
|
14
|
+
Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
|
|
15
|
+
|
|
16
|
+
/// @notice Error for invalid Diamond address
|
|
17
|
+
/// @param diamondAddress The invalid address
|
|
18
|
+
/// @param reason The reason it's invalid
|
|
19
|
+
error InvalidDiamond(address diamondAddress, string reason);
|
|
20
|
+
|
|
21
|
+
/// @notice Error for invalid facet address
|
|
22
|
+
/// @param facetAddress The invalid facet address
|
|
23
|
+
/// @param facetName The facet name
|
|
24
|
+
/// @param reason The reason it's invalid
|
|
25
|
+
error InvalidFacet(address facetAddress, string facetName, string reason);
|
|
26
|
+
|
|
27
|
+
/// @notice Assert that an address is a valid Diamond contract
|
|
28
|
+
/// @dev Checks that:
|
|
29
|
+
/// - Address is not zero
|
|
30
|
+
/// - Contract has code deployed
|
|
31
|
+
/// - Contract supports DiamondLoupe interface
|
|
32
|
+
/// @param diamond The Diamond contract address to validate
|
|
33
|
+
function assertValidDiamond(address diamond) internal view {
|
|
34
|
+
// Check non-zero address
|
|
35
|
+
if (diamond == address(0)) {
|
|
36
|
+
revert InvalidDiamond(diamond, "Diamond address is zero");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check contract has code
|
|
40
|
+
if (diamond.code.length == 0) {
|
|
41
|
+
revert InvalidDiamond(diamond, "Diamond has no code deployed");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check DiamondLoupe interface support
|
|
45
|
+
// Try calling facets() which should exist in any EIP-2535 Diamond
|
|
46
|
+
(bool success, ) = diamond.staticcall(abi.encodeWithSignature("facets()"));
|
|
47
|
+
if (!success) {
|
|
48
|
+
revert InvalidDiamond(
|
|
49
|
+
diamond,
|
|
50
|
+
"Diamond does not support DiamondLoupe (facets() call failed)"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// @notice Assert that an address is a valid facet contract
|
|
56
|
+
/// @dev Checks that:
|
|
57
|
+
/// - Address is not zero
|
|
58
|
+
/// - Contract has code deployed
|
|
59
|
+
/// @param facetAddress The facet contract address to validate
|
|
60
|
+
/// @param facetName The facet name for error messaging
|
|
61
|
+
function assertValidFacet(address facetAddress, string memory facetName) internal view {
|
|
62
|
+
// Check non-zero address
|
|
63
|
+
if (facetAddress == address(0)) {
|
|
64
|
+
revert InvalidFacet(facetAddress, facetName, "Facet address is zero");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check contract has code
|
|
68
|
+
if (facetAddress.code.length == 0) {
|
|
69
|
+
revert InvalidFacet(facetAddress, facetName, "Facet has no code deployed");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// @notice Validate address is not zero or known invalid address
|
|
74
|
+
/// @dev Use with vm.assume() in fuzz tests to filter invalid addresses
|
|
75
|
+
/// @param addr The address to validate
|
|
76
|
+
/// @return valid True if address passes basic validation
|
|
77
|
+
function isValidTestAddress(address addr) internal pure returns (bool valid) {
|
|
78
|
+
return
|
|
79
|
+
addr != address(0) &&
|
|
80
|
+
addr != address(0x000000000000000000000000000000000000dEaD) &&
|
|
81
|
+
uint160(addr) > 0xFF; // Avoid precompiles
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// @notice Validate amount is reasonable for testing
|
|
85
|
+
/// @dev Use with vm.assume() in fuzz tests to filter extreme values
|
|
86
|
+
/// @param amount The amount to validate
|
|
87
|
+
/// @return valid True if amount is within reasonable bounds
|
|
88
|
+
function isValidTestAmount(uint256 amount) internal pure returns (bool valid) {
|
|
89
|
+
return amount > 0 && amount < type(uint128).max;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// @notice Assert selector exists in Diamond
|
|
93
|
+
/// @dev Uses DiamondLoupe facetAddress() to verify selector is registered
|
|
94
|
+
/// @param diamond The Diamond contract address
|
|
95
|
+
/// @param selector The function selector to check
|
|
96
|
+
function assertSelectorExists(address diamond, bytes4 selector) internal view {
|
|
97
|
+
bytes memory callData = abi.encodeWithSignature("facetAddress(bytes4)", selector);
|
|
98
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
99
|
+
|
|
100
|
+
require(success, "facetAddress() call failed");
|
|
101
|
+
|
|
102
|
+
address facetAddress = abi.decode(returnData, (address));
|
|
103
|
+
require(facetAddress != address(0), "Selector not found in Diamond");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// @notice Assert selector routes to expected facet
|
|
107
|
+
/// @dev Uses DiamondLoupe facetAddress() to verify routing
|
|
108
|
+
/// @param diamond The Diamond contract address
|
|
109
|
+
/// @param selector The function selector to check
|
|
110
|
+
/// @param expectedFacet The expected facet address
|
|
111
|
+
function assertSelectorRouting(
|
|
112
|
+
address diamond,
|
|
113
|
+
bytes4 selector,
|
|
114
|
+
address expectedFacet
|
|
41
115
|
) internal view {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
116
|
+
bytes memory callData = abi.encodeWithSignature("facetAddress(bytes4)", selector);
|
|
117
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
118
|
+
|
|
119
|
+
require(success, "facetAddress() call failed");
|
|
120
|
+
|
|
121
|
+
address actualFacet = abi.decode(returnData, (address));
|
|
122
|
+
require(actualFacet == expectedFacet, "Selector routes to unexpected facet");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// @notice Get facet address for a selector
|
|
126
|
+
/// @dev Wrapper around DiamondLoupe facetAddress()
|
|
127
|
+
/// @param diamond The Diamond contract address
|
|
128
|
+
/// @param selector The function selector
|
|
129
|
+
/// @return facetAddress The facet address that implements this selector
|
|
130
|
+
function getFacetAddress(
|
|
131
|
+
address diamond,
|
|
132
|
+
bytes4 selector
|
|
133
|
+
) internal view returns (address facetAddress) {
|
|
134
|
+
bytes memory callData = abi.encodeWithSignature("facetAddress(bytes4)", selector);
|
|
135
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
136
|
+
|
|
137
|
+
require(success, "facetAddress() call failed");
|
|
138
|
+
facetAddress = abi.decode(returnData, (address));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// @notice Get all facets in Diamond
|
|
142
|
+
/// @dev Calls DiamondLoupe facets()
|
|
143
|
+
/// @param diamond The Diamond contract address
|
|
144
|
+
/// @return facets Array of Facet structs with address and selectors
|
|
145
|
+
function getAllFacets(address diamond) internal view returns (bytes memory facets) {
|
|
146
|
+
bytes memory callData = abi.encodeWithSignature("facets()");
|
|
147
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
148
|
+
|
|
149
|
+
require(success, "facets() call failed");
|
|
150
|
+
return returnData;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// @notice Get facet addresses
|
|
154
|
+
/// @dev Calls DiamondLoupe facetAddresses()
|
|
155
|
+
/// @param diamond The Diamond contract address
|
|
156
|
+
/// @return facetAddresses Array of all facet addresses
|
|
157
|
+
function getFacetAddresses(
|
|
158
|
+
address diamond
|
|
159
|
+
) internal view returns (address[] memory facetAddresses) {
|
|
160
|
+
bytes memory callData = abi.encodeWithSignature("facetAddresses()");
|
|
161
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
162
|
+
|
|
163
|
+
require(success, "facetAddresses() call failed");
|
|
164
|
+
facetAddresses = abi.decode(returnData, (address[]));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// @notice Get function selectors for a facet
|
|
168
|
+
/// @dev Calls DiamondLoupe facetFunctionSelectors()
|
|
169
|
+
/// @param diamond The Diamond contract address
|
|
170
|
+
/// @param facet The facet address to query
|
|
171
|
+
/// @return selectors Array of function selectors
|
|
172
|
+
function getFacetSelectors(
|
|
173
|
+
address diamond,
|
|
174
|
+
address facet
|
|
175
|
+
) internal view returns (bytes4[] memory selectors) {
|
|
176
|
+
bytes memory callData = abi.encodeWithSignature("facetFunctionSelectors(address)", facet);
|
|
177
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
178
|
+
|
|
179
|
+
require(success, "facetFunctionSelectors() call failed");
|
|
180
|
+
selectors = abi.decode(returnData, (bytes4[]));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// @notice Assert Diamond owner matches expected address
|
|
184
|
+
/// @dev Assumes owner() function exists in Diamond
|
|
185
|
+
/// @param diamond The Diamond contract address
|
|
186
|
+
/// @param expectedOwner The expected owner address
|
|
187
|
+
function assertDiamondOwner(address diamond, address expectedOwner) internal view {
|
|
188
|
+
bytes memory callData = abi.encodeWithSignature("owner()");
|
|
189
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
190
|
+
|
|
191
|
+
require(success, "owner() call failed");
|
|
192
|
+
|
|
193
|
+
address actualOwner = abi.decode(returnData, (address));
|
|
194
|
+
require(actualOwner == expectedOwner, "Diamond owner does not match expected");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// @notice Get Diamond owner address
|
|
198
|
+
/// @dev Assumes owner() function exists in Diamond
|
|
199
|
+
/// @param diamond The Diamond contract address
|
|
200
|
+
/// @return owner The current Diamond owner
|
|
201
|
+
function getDiamondOwner(address diamond) internal view returns (address owner) {
|
|
202
|
+
bytes memory callData = abi.encodeWithSignature("owner()");
|
|
203
|
+
(bool success, bytes memory returnData) = diamond.staticcall(callData);
|
|
204
|
+
|
|
205
|
+
require(success, "owner() call failed");
|
|
206
|
+
owner = abi.decode(returnData, (address));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// @notice Format bytes4 selector as hex string
|
|
210
|
+
/// @dev Utility for logging and debugging
|
|
211
|
+
/// @param selector The bytes4 selector
|
|
212
|
+
/// @return hexString The hex string representation
|
|
213
|
+
function toHexString(bytes4 selector) internal pure returns (string memory hexString) {
|
|
214
|
+
bytes memory buffer = new bytes(10);
|
|
215
|
+
buffer[0] = "0";
|
|
216
|
+
buffer[1] = "x";
|
|
217
|
+
|
|
218
|
+
bytes memory hexChars = "0123456789abcdef";
|
|
219
|
+
bytes memory selectorBytes = abi.encodePacked(selector);
|
|
220
|
+
|
|
221
|
+
for (uint256 i = 0; i < 4; i++) {
|
|
222
|
+
uint8 value = uint8(selectorBytes[i]);
|
|
223
|
+
buffer[2 + i * 2] = hexChars[value >> 4];
|
|
224
|
+
buffer[3 + i * 2] = hexChars[value & 0x0f];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return string(buffer);
|
|
228
|
+
}
|
|
45
229
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
230
|
+
/// @notice Create a bounded random address for fuzzing
|
|
231
|
+
/// @dev Ensures address is valid for testing
|
|
232
|
+
/// @param seed The fuzzing seed value
|
|
233
|
+
/// @return addr A valid test address
|
|
234
|
+
function boundAddress(uint256 seed) internal pure returns (address addr) {
|
|
235
|
+
// Create address from seed, ensuring it's valid
|
|
236
|
+
addr = address(uint160(seed));
|
|
237
|
+
|
|
238
|
+
// Re-hash if address is invalid
|
|
239
|
+
while (!isValidTestAddress(addr)) {
|
|
240
|
+
seed = uint256(keccak256(abi.encodePacked(seed)));
|
|
241
|
+
addr = address(uint160(seed));
|
|
49
242
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
243
|
+
|
|
244
|
+
return addr;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// @notice Create a bounded random amount for fuzzing
|
|
248
|
+
/// @dev Ensures amount is reasonable for testing
|
|
249
|
+
/// @param seed The fuzzing seed value
|
|
250
|
+
/// @param min The minimum amount (inclusive)
|
|
251
|
+
/// @param max The maximum amount (inclusive)
|
|
252
|
+
/// @return amount A valid test amount
|
|
253
|
+
function boundAmount(
|
|
254
|
+
uint256 seed,
|
|
255
|
+
uint256 min,
|
|
256
|
+
uint256 max
|
|
257
|
+
) internal pure returns (uint256 amount) {
|
|
258
|
+
require(max >= min, "max must be >= min");
|
|
259
|
+
require(max < type(uint128).max, "max must be < uint128 max");
|
|
260
|
+
|
|
261
|
+
amount = min + (seed % (max - min + 1));
|
|
262
|
+
return amount;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// @notice Compare two bytes arrays for equality
|
|
266
|
+
/// @dev Utility for comparing ABI-encoded data
|
|
267
|
+
/// @param a First bytes array
|
|
268
|
+
/// @param b Second bytes array
|
|
269
|
+
/// @return equal True if arrays are equal
|
|
270
|
+
function bytesEqual(bytes memory a, bytes memory b) internal pure returns (bool equal) {
|
|
271
|
+
return keccak256(a) == keccak256(b);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// @notice Compare two bytes4 arrays for equality
|
|
275
|
+
/// @dev Utility for comparing selector arrays
|
|
276
|
+
/// @param a First array
|
|
277
|
+
/// @param b Second array
|
|
278
|
+
/// @return equal True if arrays contain same selectors (order doesn't matter)
|
|
279
|
+
function selectorsEqual(
|
|
280
|
+
bytes4[] memory a,
|
|
281
|
+
bytes4[] memory b
|
|
282
|
+
) internal pure returns (bool equal) {
|
|
283
|
+
if (a.length != b.length) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Simple O(n²) comparison for small arrays (typical in Diamond tests)
|
|
288
|
+
for (uint256 i = 0; i < a.length; i++) {
|
|
289
|
+
bool found = false;
|
|
290
|
+
for (uint256 j = 0; j < b.length; j++) {
|
|
291
|
+
if (a[i] == b[j]) {
|
|
292
|
+
found = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!found) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// @notice Take a snapshot of current blockchain state
|
|
305
|
+
/// @dev Uses Foundry's vm.snapshotState() to save state
|
|
306
|
+
/// @dev Only works on networks that support snapshots (Hardhat, Anvil)
|
|
307
|
+
/// @return snapshotId The snapshot identifier to use for reverting
|
|
308
|
+
function snapshotState() internal returns (uint256 snapshotId) {
|
|
309
|
+
snapshotId = vm.snapshotState();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// @notice Revert blockchain state to a previously saved snapshot
|
|
313
|
+
/// @dev Uses Foundry's vm.revertToState() to restore state
|
|
314
|
+
/// @dev The snapshot is consumed after reverting
|
|
315
|
+
/// @param snapshotId The snapshot identifier from snapshotState()
|
|
316
|
+
/// @return success True if revert was successful
|
|
317
|
+
function revertToSnapshot(uint256 snapshotId) internal returns (bool success) {
|
|
318
|
+
success = vm.revertToState(snapshotId);
|
|
95
319
|
}
|
|
96
320
|
}
|