@bananapus/core-v6 0.0.13 → 0.0.15

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.
Files changed (38) hide show
  1. package/STYLE_GUIDE.md +134 -41
  2. package/foundry.toml +3 -3
  3. package/package.json +4 -4
  4. package/remappings.txt +1 -1
  5. package/script/Deploy.s.sol +2 -4
  6. package/script/DeployPeriphery.s.sol +2 -15
  7. package/script/helpers/CoreDeploymentLib.sol +1 -2
  8. package/src/JBController.sol +1 -1
  9. package/src/interfaces/IJBController.sol +0 -1
  10. package/src/interfaces/IJBPayoutTerminal.sol +0 -1
  11. package/test/ComprehensiveInvariant.t.sol +0 -3
  12. package/test/{AuditExploits.t.sol → CoreExploitTests.t.sol} +1 -3
  13. package/test/EntryPointPermutations.t.sol +1 -2
  14. package/test/FlashLoanAttacks.t.sol +0 -1
  15. package/test/PermissionEscalation.t.sol +1 -12
  16. package/test/SplitLoopTests.t.sol +1 -3
  17. package/test/TestCashOut.sol +0 -1
  18. package/test/TestCashOutCountFor.sol +0 -1
  19. package/test/TestFeeProcessingFailure.sol +1 -6
  20. package/test/TestMigrationHeldFees.sol +1 -10
  21. package/test/TestPermissions.sol +1 -2
  22. package/test/TestRulesetQueuingStress.sol +8 -1
  23. package/test/TestRulesetWeightCaching.sol +0 -1
  24. package/test/WeirdTokenTests.t.sol +1 -1
  25. package/test/fork/TestChainlinkPriceFeedFork.sol +249 -0
  26. package/test/formal/BondingCurveProperties.t.sol +0 -1
  27. package/test/helpers/JBTest.sol +6 -6
  28. package/test/helpers/TestBaseWorkflow.sol +2 -1
  29. package/test/invariants/Phase3DeepInvariant.t.sol +0 -3
  30. package/test/invariants/handlers/Phase3Handler.sol +0 -1
  31. package/test/invariants/handlers/TokensHandler.sol +0 -1
  32. package/test/mock/ERC2771ForwarderMock.sol +1 -1
  33. package/test/mock/MockERC20.sol +1 -1
  34. package/test/mock/MockMaliciousBeneficiary.sol +0 -1
  35. package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +3 -3
  36. package/test/units/static/JBMetadataResolver/{TestMetadataResolverM20M21.sol → TestMetadataResolverEdgeCases.sol} +4 -4
  37. package/test/units/static/JBRulesets/TestRulesets.sol +0 -1
  38. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +1 -1
package/STYLE_GUIDE.md CHANGED
@@ -17,8 +17,6 @@ src/
17
17
 
18
18
  One contract/interface/struct/enum per file. Name the file after the type it contains.
19
19
 
20
- **Structs, enums, libraries, and interfaces always go in their subdirectories** (`src/structs/`, `src/enums/`, `src/libraries/`, `src/interfaces/`) — never inline in contract files or placed in `src/` root. This keeps type definitions discoverable and import paths consistent across repos.
21
-
22
20
  ## Pragma Versions
23
21
 
24
22
  ```solidity
@@ -106,14 +104,6 @@ contract JBExample is JBPermissioned, IJBExample {
106
104
  // -------------------------- constructor ---------------------------- //
107
105
  //*********************************************************************//
108
106
 
109
- //*********************************************************************//
110
- // ---------------------- receive / fallback ------------------------- //
111
- //*********************************************************************//
112
-
113
- //*********************************************************************//
114
- // --------------------------- modifiers ----------------------------- //
115
- //*********************************************************************//
116
-
117
107
  //*********************************************************************//
118
108
  // ---------------------- external transactions ---------------------- //
119
109
  //*********************************************************************//
@@ -141,28 +131,23 @@ contract JBExample is JBPermissioned, IJBExample {
141
131
  ```
142
132
 
143
133
  **Section order:**
144
- 1. `using` declarations
145
- 2. Custom errors
146
- 3. Public constants
147
- 4. Internal constants
148
- 5. Public immutable stored properties
149
- 6. Internal immutable stored properties
150
- 7. Public stored properties
151
- 8. Internal stored properties
152
- 9. Constructor
153
- 10. `receive` / `fallback`
154
- 11. Modifiers
155
- 12. External transactions
156
- 13. External views
157
- 14. Public transactions
158
- 15. Internal helpers
159
- 16. Internal views
160
- 17. Private helpers
134
+ 1. Custom errors
135
+ 2. Public constants
136
+ 3. Internal constants
137
+ 4. Public immutable stored properties
138
+ 5. Internal immutable stored properties
139
+ 6. Public stored properties
140
+ 7. Internal stored properties
141
+ 8. Constructor
142
+ 9. External transactions
143
+ 10. External views
144
+ 11. Public transactions
145
+ 12. Internal helpers
146
+ 13. Internal views
147
+ 14. Private helpers
161
148
 
162
149
  Functions are alphabetized within each section.
163
150
 
164
- **Events:** Events are declared in interfaces only, never in implementation contracts. Implementations inherit events from their interface and emit them unqualified. This keeps the ABI definition in one place and allows tests to use interface-qualified event expectations (e.g., `emit IJBController.LaunchProject(...)`).
165
-
166
151
  ## Interface Structure
167
152
 
168
153
  ```solidity
@@ -324,7 +309,7 @@ try hook.afterPayRecordedWith(context) {} catch (bytes memory reason) {
324
309
 
325
310
  ### foundry.toml
326
311
 
327
- Standard config for `@bananapus/core-v6`:
312
+ Standard config across all repos:
328
313
 
329
314
  ```toml
330
315
  [profile.default]
@@ -334,9 +319,6 @@ optimizer_runs = 200
334
319
  libs = ["node_modules", "lib"]
335
320
  fs_permissions = [{ access = "read-write", path = "./"}]
336
321
 
337
- [profile.ci_sizes]
338
- optimizer_runs = 200
339
-
340
322
  [fuzz]
341
323
  runs = 4096
342
324
 
@@ -351,7 +333,14 @@ multiline_func_header = "all"
351
333
  wrap_comments = true
352
334
  ```
353
335
 
354
- This is the standard config with no deviations.
336
+ **Optional sections (add only when needed):**
337
+ - `[rpc_endpoints]` — repos with fork tests. Maps named endpoints to env vars (e.g. `ethereum = "${RPC_ETHEREUM_MAINNET}"`).
338
+ - `[profile.ci_sizes]` — only when CI needs different optimizer settings than defaults for the size check step (e.g. `optimizer_runs = 200` when the default profile uses a lower value).
339
+
340
+ **Common variations:**
341
+ - `via_ir = true` when hitting stack-too-deep
342
+ - `optimizer = false` when optimization causes stack-too-deep
343
+ - `optimizer_runs` reduced when deep struct nesting causes stack-too-deep at 200 runs
355
344
 
356
345
  ### CI Workflows
357
346
 
@@ -381,8 +370,10 @@ jobs:
381
370
  uses: foundry-rs/foundry-toolchain@v1
382
371
  - name: Run tests
383
372
  run: forge test --fail-fast --summary --detailed --skip "*/script/**"
373
+ env:
374
+ RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
384
375
  - name: Check contract sizes
385
- run: FOUNDRY_PROFILE=ci_sizes forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
376
+ run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
386
377
  ```
387
378
 
388
379
  **lint.yml:**
@@ -404,14 +395,63 @@ jobs:
404
395
  run: forge fmt --check
405
396
  ```
406
397
 
398
+ **slither.yml** (repos with `src/` contracts only):
399
+ ```yaml
400
+ name: slither
401
+ on:
402
+ pull_request:
403
+ branches:
404
+ - main
405
+ push:
406
+ branches:
407
+ - main
408
+ jobs:
409
+ analyze:
410
+ runs-on: ubuntu-latest
411
+ steps:
412
+ - uses: actions/checkout@v4
413
+ with:
414
+ submodules: recursive
415
+ - uses: actions/setup-node@v4
416
+ with:
417
+ node-version: latest
418
+ - name: Install npm dependencies
419
+ run: npm install --omit=dev
420
+ - name: Install Foundry
421
+ uses: foundry-rs/foundry-toolchain@v1
422
+ - name: Run slither
423
+ uses: crytic/slither-action@v0.3.1
424
+ with:
425
+ slither-config: slither-ci.config.json
426
+ fail-on: medium
427
+ ```
428
+
429
+ **slither-ci.config.json:**
430
+ ```json
431
+ {
432
+ "detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
433
+ "exclude_informational": true,
434
+ "exclude_low": false,
435
+ "exclude_medium": false,
436
+ "exclude_high": false,
437
+ "disable_color": false,
438
+ "filter_paths": "(mocks/|test/|node_modules/|lib/)",
439
+ "legacy_ast": false
440
+ }
441
+ ```
442
+
443
+ **Variations:**
444
+ - Deployer-only repos (no `src/`, only `script/`) skip slither entirely — the action's internal `forge build` skips `test/` and `script/` by default, leaving nothing to compile.
445
+ - Use inline `// slither-disable-next-line <detector>` to suppress known false positives rather than adding to `detectors_to_exclude` in the config. The comment must be on the line immediately before the flagged expression.
446
+
407
447
  ### package.json
408
448
 
409
449
  ```json
410
450
  {
411
- "name": "@bananapus/core-v6",
451
+ "name": "@bananapus/package-name-v6",
412
452
  "version": "x.x.x",
413
453
  "license": "MIT",
414
- "repository": { "type": "git", "url": "git+https://github.com/Bananapus/nana-core-v6.git" },
454
+ "repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
415
455
  "engines": { "node": ">=20.0.0" },
416
456
  "scripts": {
417
457
  "test": "forge test",
@@ -428,13 +468,62 @@ jobs:
428
468
 
429
469
  ### remappings.txt
430
470
 
431
- Every repo has a `remappings.txt`. Minimal content:
471
+ Every repo has a `remappings.txt` as the **single source of truth** for import remappings. Never add remappings to `foundry.toml`.
472
+
473
+ **Principle:** Import paths in Solidity source must match npm package names exactly. With `libs = ["node_modules", "lib"]`, Foundry auto-resolves `@scope/package/path/File.sol` → `node_modules/@scope/package/path/File.sol`. No remapping needed for packages installed as real directories.
474
+
475
+ **Note:** Auto-resolution does **not** work for symlinked packages (e.g. npm workspace links). Workspace repos like `deploy-all-v6` and `nana-cli-v6` need explicit `@scope/package/=node_modules/@scope/package/` remappings for each symlinked dependency.
476
+
477
+ **Minimal content** (most repos):
478
+
479
+ ```
480
+ forge-std/=lib/forge-std/src/
481
+ ```
482
+
483
+ Only add extra remappings for:
484
+ - **`forge-std`** — always needed (git submodule with `src/` subdirectory)
485
+ - **Repo-specific `lib/` submodules** that have no npm package (e.g., `hookmate/=lib/hookmate/src/`)
486
+ - **Symlinked npm packages** — need explicit `@scope/package/=node_modules/@scope/package/` entries
487
+ - **Nested transitive deps** — e.g., `@chainlink/contracts-ccip/` nested inside `@bananapus/suckers-v6/node_modules/`
488
+
489
+ **Never add remappings for:**
490
+ - npm packages that match their import path and are installed as real directories — they auto-resolve
491
+ - Short-form aliases (e.g., `@bananapus/core/` → `@bananapus/core-v6/src/`) — fix the import instead
492
+ - Packages available via npm that are also git submodules — remove the submodule, use npm
432
493
 
494
+ **Import path convention:**
495
+
496
+ | Package | Import path | Resolves to |
497
+ |---------|------------|-------------|
498
+ | `@bananapus/core-v6` | `@bananapus/core-v6/src/libraries/JBConstants.sol` | `node_modules/@bananapus/core-v6/src/...` |
499
+ | `@openzeppelin/contracts` | `@openzeppelin/contracts/token/ERC20/IERC20.sol` | `node_modules/@openzeppelin/contracts/...` |
500
+ | `@uniswap/v4-core` | `@uniswap/v4-core/src/interfaces/IPoolManager.sol` | `node_modules/@uniswap/v4-core/src/...` |
501
+
502
+ ### Linting
503
+
504
+ Solar (Foundry's built-in linter) runs automatically during `forge build`. It scans all `.sol` files in `libs` directories, including `node_modules`.
505
+
506
+ **All test helpers must use relative imports** (e.g. `../../src/structs/JBRuleset.sol`), not bare `src/` imports. This ensures solar can resolve paths when the helper is consumed via npm in downstream repos.
507
+
508
+ ### Fork Tests
509
+
510
+ Fork tests use named RPC endpoints defined in `[rpc_endpoints]` of `foundry.toml`. No skip guards — fork tests should hard-fail if the RPC endpoint is unavailable, making CI failures explicit.
511
+
512
+ ```solidity
513
+ function setUp() public {
514
+ vm.createSelectFork("ethereum");
515
+ // ... setup code
516
+ }
433
517
  ```
434
- @sphinx-labs/contracts/=lib/sphinx/packages/contracts/contracts/foundry
518
+
519
+ The endpoint name (e.g. `"ethereum"`) maps to an env var via `foundry.toml`:
520
+
521
+ ```toml
522
+ [rpc_endpoints]
523
+ ethereum = "${RPC_ETHEREUM_MAINNET}"
435
524
  ```
436
525
 
437
- Additional mappings as needed for repo-specific dependencies.
526
+ For multi-chain fork tests, add all needed endpoints.
438
527
 
439
528
  ### Formatting
440
529
 
@@ -462,4 +551,8 @@ CI checks formatting via `forge fmt --check`.
462
551
 
463
552
  ### Contract Size Checks
464
553
 
465
- CI runs `FOUNDRY_PROFILE=ci_sizes forge build --sizes` to catch contracts approaching the 24KB limit. The `ci_sizes` profile uses `optimizer_runs = 200` for realistic size measurement even when the default profile has different optimizer settings.
554
+ CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
555
+
556
+ ## Repo-Specific Deviations
557
+
558
+ None. This repo follows the standard configuration exactly.
package/foundry.toml CHANGED
@@ -5,9 +5,6 @@ optimizer_runs = 200
5
5
  libs = ["node_modules", "lib"]
6
6
  fs_permissions = [{ access = "read-write", path = "./"}]
7
7
 
8
- [profile.ci_sizes]
9
- optimizer_runs = 200
10
-
11
8
  [fuzz]
12
9
  runs = 4096
13
10
 
@@ -16,6 +13,9 @@ runs = 1024
16
13
  depth = 100
17
14
  fail_on_revert = false
18
15
 
16
+ [rpc_endpoints]
17
+ ethereum = "${RPC_ETHEREUM_MAINNET}"
18
+
19
19
  [fmt]
20
20
  number_underscore = "thousands"
21
21
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,10 +26,10 @@
26
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
27
27
  },
28
28
  "dependencies": {
29
- "@bananapus/permission-ids-v6": "^0.0.6",
29
+ "@bananapus/permission-ids-v6": "^0.0.8",
30
30
  "@chainlink/contracts": "^1.3.0",
31
- "@openzeppelin/contracts": "^5.2.0",
32
- "@prb/math": "^4.1.0",
31
+ "@openzeppelin/contracts": "^5.6.1",
32
+ "@prb/math": "^4.1.1",
33
33
  "@uniswap/permit2": "github:Uniswap/permit2"
34
34
  },
35
35
  "devDependencies": {
package/remappings.txt CHANGED
@@ -1 +1 @@
1
- @sphinx-labs/contracts/=lib/sphinx/packages/contracts/contracts/foundry
1
+ forge-std/=lib/forge-std/src/
@@ -1,9 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import "@sphinx-labs/contracts/SphinxPlugin.sol";
5
- import {Script, stdJson, VmSafe} from "forge-std/Script.sol";
6
- import {CoreDeploymentLib} from "./helpers/CoreDeploymentLib.sol";
4
+ import "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
5
+ import {Script} from "forge-std/Script.sol";
7
6
 
8
7
  import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
9
8
  import {JBPermissions} from "src/JBPermissions.sol";
@@ -16,7 +15,6 @@ import {JBTokens} from "src/JBTokens.sol";
16
15
  import {JBSplits} from "src/JBSplits.sol";
17
16
  import {JBFeelessAddresses} from "src/JBFeelessAddresses.sol";
18
17
  import {JBFundAccessLimits} from "src/JBFundAccessLimits.sol";
19
- import {JBController} from "src/JBController.sol";
20
18
  import {JBTerminalStore} from "src/JBTerminalStore.sol";
21
19
  import {JBMultiTerminal} from "src/JBMultiTerminal.sol";
22
20
 
@@ -1,15 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import "@sphinx-labs/contracts/SphinxPlugin.sol";
5
- import {Script, stdJson, VmSafe} from "forge-std/Script.sol";
4
+ import "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
5
+ import {Script} from "forge-std/Script.sol";
6
6
  import {CoreDeployment, CoreDeploymentLib} from "./helpers/CoreDeploymentLib.sol";
7
7
 
8
- import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
9
8
  import {IJBPriceFeed} from "src/interfaces/IJBPriceFeed.sol";
10
- import {JBPermissions} from "src/JBPermissions.sol";
11
- import {JBProjects} from "src/JBProjects.sol";
12
- import {JBPrices} from "src/JBPrices.sol";
13
9
  import {JBDeadline3Hours} from "src/periphery/JBDeadline3Hours.sol";
14
10
  import {JBDeadline1Day} from "src/periphery/JBDeadline1Day.sol";
15
11
  import {JBDeadline3Days} from "src/periphery/JBDeadline3Days.sol";
@@ -18,16 +14,7 @@ import {JBMatchingPriceFeed} from "src/periphery/JBMatchingPriceFeed.sol";
18
14
  import {JBChainlinkV3PriceFeed, AggregatorV3Interface} from "src/JBChainlinkV3PriceFeed.sol";
19
15
  import {AggregatorV2V3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol";
20
16
  import {JBChainlinkV3SequencerPriceFeed} from "src/JBChainlinkV3SequencerPriceFeed.sol";
21
- import {JBRulesets} from "src/JBRulesets.sol";
22
- import {JBDirectory} from "src/JBDirectory.sol";
23
- import {JBERC20} from "src/JBERC20.sol";
24
- import {JBTokens} from "src/JBTokens.sol";
25
- import {JBSplits} from "src/JBSplits.sol";
26
- import {JBFeelessAddresses} from "src/JBFeelessAddresses.sol";
27
- import {JBFundAccessLimits} from "src/JBFundAccessLimits.sol";
28
17
  import {JBController} from "src/JBController.sol";
29
- import {JBTerminalStore} from "src/JBTerminalStore.sol";
30
- import {JBMultiTerminal} from "src/JBMultiTerminal.sol";
31
18
 
32
19
  import {JBConstants} from "src/libraries/JBConstants.sol";
33
20
  import {JBCurrencyIds} from "src/libraries/JBCurrencyIds.sol";
@@ -16,9 +16,8 @@ import {JBFundAccessLimits} from "../../src/JBFundAccessLimits.sol";
16
16
  import {JBController} from "../../src/JBController.sol";
17
17
  import {JBTerminalStore} from "../../src/JBTerminalStore.sol";
18
18
  import {JBMultiTerminal} from "../../src/JBMultiTerminal.sol";
19
- import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";
20
19
 
21
- import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/SphinxConstants.sol";
20
+ import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/contracts/foundry/SphinxConstants.sol";
22
21
 
23
22
  struct CoreDeployment {
24
23
  JBPermissions permissions;
@@ -187,7 +187,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
187
187
  /// @dev Can only be called by the directory.
188
188
  /// @param from The controller being migrated from.
189
189
  /// @param projectId The ID of the project that migrated to this controller.
190
- function afterReceiveMigrationFrom(IERC165 from, uint256 projectId) external override {
190
+ function afterReceiveMigrationFrom(IERC165 from, uint256 projectId) external view override {
191
191
  from; // Suppress unused variable warning.
192
192
  projectId; // Suppress unused variable warning.
193
193
 
@@ -12,7 +12,6 @@ import {IJBProjects} from "./IJBProjects.sol";
12
12
  import {IJBProjectUriRegistry} from "./IJBProjectUriRegistry.sol";
13
13
  import {IJBRulesets} from "./IJBRulesets.sol";
14
14
  import {IJBSplits} from "./IJBSplits.sol";
15
- import {IJBTerminal} from "./IJBTerminal.sol";
16
15
  import {IJBToken} from "./IJBToken.sol";
17
16
  import {IJBTokens} from "./IJBTokens.sol";
18
17
  import {JBApprovalStatus} from "./../enums/JBApprovalStatus.sol";
@@ -1,7 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {IJBSplits} from "./IJBSplits.sol";
5
4
  import {IJBTerminal} from "./IJBTerminal.sol";
6
5
  import {JBSplit} from "../structs/JBSplit.sol";
7
6
 
@@ -266,9 +266,6 @@ contract ComprehensiveInvariant_Local is StdInvariant, TestBaseWorkflow {
266
266
  uint256 totalOut =
267
267
  handler.ghost_totalCashedOut() + handler.ghost_totalPaidOut() + handler.ghost_totalAllowanceUsed();
268
268
 
269
- uint256 projectBalance =
270
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
271
-
272
269
  // Fees go to project #1, so total funds are conserved within the terminal
273
270
  assertGe(totalIn, totalOut, "COMP6: Ghost conservation - total funds in must be >= total funds out");
274
271
  }
@@ -682,7 +682,7 @@ contract EdgeCases_Local is TestBaseWorkflow {
682
682
  // Cash out all remaining tokens.
683
683
  uint256 tokenBalance = _tokens.totalBalanceOf(_beneficiary, projectId);
684
684
  vm.prank(_beneficiary);
685
- uint256 reclaimAmount = _terminal.cashOutTokensOf({
685
+ _terminal.cashOutTokensOf({
686
686
  holder: _beneficiary,
687
687
  projectId: projectId,
688
688
  cashOutCount: tokenBalance,
@@ -1451,8 +1451,6 @@ contract EdgeCases_Local is TestBaseWorkflow {
1451
1451
  vm.warp(block.timestamp + 2_419_200 + 1); // 28 days + 1 second
1452
1452
 
1453
1453
  // Record terminal ETH before processing.
1454
- uint256 terminalBalanceBefore = address(_terminal).balance;
1455
-
1456
1454
  // Process both held fees.
1457
1455
  _terminal.processHeldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
1458
1456
 
@@ -4,7 +4,6 @@ pragma solidity ^0.8.6;
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
6
6
  import {JBConstants} from "../src/libraries/JBConstants.sol";
7
- import {JBFees} from "../src/libraries/JBFees.sol";
8
7
 
9
8
  /// @title EntryPointPermutations
10
9
  /// @notice Systematically tests every JBMultiTerminal external function with edge-case parameters
@@ -452,7 +451,7 @@ contract EntryPointPermutations_Local is TestBaseWorkflow {
452
451
  );
453
452
 
454
453
  // Held fees should be reduced (partially or fully returned)
455
- JBFee[] memory feesAfter = jbMultiTerminal().heldFeesOf(projectStandard, JBConstants.NATIVE_TOKEN, 100);
454
+ jbMultiTerminal().heldFeesOf(projectStandard, JBConstants.NATIVE_TOKEN, 100);
456
455
 
457
456
  if (feesBefore.length > 0) {
458
457
  // Either fewer fees or smaller amounts
@@ -254,7 +254,6 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
254
254
 
255
255
  // Both should get proportional shares (with cashOutTax reducing it)
256
256
  // Key check: they should get roughly equal amounts since they have equal tokens
257
- uint256 diff = aliceReclaim > bobReclaim ? aliceReclaim - bobReclaim : bobReclaim - aliceReclaim;
258
257
  // Alice cashes out first, so she gets slightly more due to reduced supply.
259
258
  // But the proportional split should be reasonable.
260
259
  assertTrue(aliceReclaim > 0, "Alice should get some reclaim");
@@ -500,7 +500,7 @@ contract PermissionEscalation_Local is TestBaseWorkflow {
500
500
  // Test 10: ROOT on project 2 has no power over project 3
501
501
  // ═══════════════════════════════════════════════════════════════════
502
502
 
503
- function test_permission_rootOnProject_doesNotAffectOtherProject() public {
503
+ function test_permission_rootOnProject_doesNotAffectOtherProject() public view {
504
504
  // Alice has ROOT on project 2 (from setUp)
505
505
  // Alice should NOT have any permissions on project 3
506
506
  bool hasPermOnProject3 = jbPermissions()
@@ -638,17 +638,6 @@ contract PermissionEscalation_Local is TestBaseWorkflow {
638
638
  // Verify the forwarder is set up correctly
639
639
  assertTrue(forwarder != address(0), "Trusted forwarder should be set");
640
640
 
641
- // Construct the calldata with Bob's address appended (ERC2771 pattern)
642
- bytes memory callData = abi.encodeWithSelector(
643
- IJBPermissions.hasPermission.selector,
644
- bob,
645
- projectOwner,
646
- projectId2,
647
- JBPermissionIds.CASH_OUT_TOKENS,
648
- false,
649
- false
650
- );
651
-
652
641
  // Direct call should return true
653
642
  bool hasPerm = jbPermissions()
654
643
  .hasPermission({
@@ -463,10 +463,8 @@ contract SplitLoopTests_Local is TestBaseWorkflow {
463
463
 
464
464
  _payProject(pid, address(0xBA1E), 10 ether);
465
465
 
466
- uint256 totalBefore = address(jbMultiTerminal()).balance;
467
-
468
466
  vm.prank(projectOwner);
469
- uint256 netLeftover = jbMultiTerminal()
467
+ jbMultiTerminal()
470
468
  .sendPayoutsOf({
471
469
  projectId: pid,
472
470
  token: JBConstants.NATIVE_TOKEN,
@@ -2,7 +2,6 @@
2
2
  pragma solidity ^0.8.6;
3
3
 
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
- import {JBCashOuts} from "../src/libraries/JBCashOuts.sol";
6
5
 
7
6
  // Projects can issue a token, be paid to receieve claimed tokens, burn some of the claimed tokens, cash out the rest
8
7
  // of
@@ -2,7 +2,6 @@
2
2
  pragma solidity ^0.8.17;
3
3
 
4
4
  import "forge-std/Test.sol";
5
- import {mulDiv} from "@prb/math/src/Common.sol";
6
5
  import {JBCashOuts} from "../src/libraries/JBCashOuts.sol";
7
6
  import {JBConstants} from "../src/libraries/JBConstants.sol";
8
7
 
@@ -153,9 +153,6 @@ contract TestFeeProcessingFailure_Local is TestBaseWorkflow {
153
153
  // For this test, verify the FeeReverted event is emitted when fee processing fails.
154
154
  // We'll manipulate the directory to return address(0) for fee project's terminal.
155
155
 
156
- // Record balance before payout.
157
- uint256 projectBalanceBefore = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
158
-
159
156
  // The fee is taken during sendPayoutsOf. Under normal conditions, it goes to project #1.
160
157
  // The _processFee try-catch handles failures gracefully.
161
158
  // We verify that the normal path works (fee is deducted from payout and sent to fee project).
@@ -168,8 +165,6 @@ contract TestFeeProcessingFailure_Local is TestBaseWorkflow {
168
165
  minTokensPaidOut: 0
169
166
  });
170
167
 
171
- uint256 projectBalanceAfter = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
172
-
173
168
  // Fee was deducted from the payout — project balance is now only the surplus (if any).
174
169
  // The fee was 2.5% of the payout amount.
175
170
  uint256 feeAmount = JBFees.feeAmountFrom({amountBeforeFee: PAY_AMOUNT, feePercent: _terminal.FEE()});
@@ -178,7 +173,7 @@ contract TestFeeProcessingFailure_Local is TestBaseWorkflow {
178
173
 
179
174
  /// @notice Held fee processing: when fee payment reverts, the FeeReverted event is emitted
180
175
  /// and the fee amount is credited back to the project balance via _recordAddedBalanceFor.
181
- function test_heldFeeProcessing_revert_refundsToProject() external {
176
+ function test_heldFeeProcessing_revert_refundsToProject() external view {
182
177
  // This test requires holdFees=true ruleset — we test the principle:
183
178
  // When _processFee's try block reverts, the catch block calls _recordAddedBalanceFor.
184
179
  // This returns the fee amount to the project's terminal store balance.
@@ -144,11 +144,8 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
144
144
  assertGt(heldFees.length, 0, "Should have held fees after payout");
145
145
 
146
146
  // Step 3: Migrate balance to terminal2.
147
- uint256 balanceBefore = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
148
-
149
147
  vm.prank(_projectOwner);
150
- uint256 migrated =
151
- _terminal.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, IJBTerminal(address(_terminal2)));
148
+ _terminal.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, IJBTerminal(address(_terminal2)));
152
149
 
153
150
  // Step 4: Verify old terminal has no balance but still has held fees.
154
151
  uint256 balanceAfter = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
@@ -231,16 +228,10 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
231
228
  // Warp past holding period.
232
229
  vm.warp(block.timestamp + 28 days + 1);
233
230
 
234
- // Fee project balance before attempting to process.
235
- uint256 feeBalanceBefore = _store.balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
236
-
237
231
  // Try to process held fees — the fees still exist but the old terminal has no ETH.
238
232
  // The _processFee try-catch will catch the revert and credit the fee amount back to the project.
239
233
  _terminal.processHeldFeesOf(_projectId, JBConstants.NATIVE_TOKEN, 100);
240
234
 
241
- // Fee project should NOT have received fees (old terminal had no ETH to transfer).
242
- uint256 feeBalanceAfter = _store.balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
243
-
244
235
  // The fee processing attempted but the terminal has no actual ETH.
245
236
  // The _recordAddedBalanceFor in the catch block inflates the store balance
246
237
  // without actual funds — this is a phantom balance.
@@ -2,9 +2,8 @@
2
2
  pragma solidity ^0.8.6;
3
3
 
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
- import {JBTest} from "./helpers/JBTest.sol";
6
5
 
7
- contract TestPermissions_Local is TestBaseWorkflow, JBTest {
6
+ contract TestPermissions_Local is TestBaseWorkflow {
8
7
  IJBController private _controller;
9
8
  JBRulesetMetadata private _metadata;
10
9
  IJBTerminal private _terminal;
@@ -709,7 +709,14 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
709
709
  }
710
710
 
711
711
  /// @notice Fuzz: deriveStartFrom always returns a value >= mustStartAtOrAfter and aligned to duration.
712
- function testFuzz_deriveStartFrom_alignment(uint48 baseStart, uint32 duration, uint48 mustStartAfter) external {
712
+ function testFuzz_deriveStartFrom_alignment(
713
+ uint48 baseStart,
714
+ uint32 duration,
715
+ uint48 mustStartAfter
716
+ )
717
+ external
718
+ view
719
+ {
713
720
  // Bound to reasonable values.
714
721
  baseStart = uint48(bound(baseStart, 1, type(uint48).max / 2));
715
722
  duration = uint32(bound(duration, 1, type(uint32).max / 2));
@@ -2,7 +2,6 @@
2
2
  pragma solidity >=0.8.6;
3
3
 
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
- import {MockPriceFeed} from "./mock/MockPriceFeed.sol";
6
5
 
7
6
  // A ruleset's weight can be cached to make larger intervals calculable while staying within the gas limit.
8
7
  contract TestRulesetWeightCaching_Local is TestBaseWorkflow {
@@ -3,7 +3,7 @@ pragma solidity ^0.8.6;
3
3
 
4
4
  import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
6
- import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7
7
 
8
8
  /// @notice Tests for weird/non-standard ERC-20 tokens: fee-on-transfer, rebasing, return-false, low/high decimals.
9
9
  contract WeirdTokenTests_Local is TestBaseWorkflow {
@@ -0,0 +1,249 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
7
+
8
+ import {JBChainlinkV3PriceFeed} from "../../src/JBChainlinkV3PriceFeed.sol";
9
+ import {JBPrices} from "../../src/JBPrices.sol";
10
+ import {IJBDirectory} from "../../src/interfaces/IJBDirectory.sol";
11
+ import {IJBPermissions} from "../../src/interfaces/IJBPermissions.sol";
12
+ import {IJBPriceFeed} from "../../src/interfaces/IJBPriceFeed.sol";
13
+ import {IJBProjects} from "../../src/interfaces/IJBProjects.sol";
14
+
15
+ /// @notice Fork tests for JBChainlinkV3PriceFeed and JBPrices against live Chainlink oracles on Ethereum mainnet.
16
+ contract TestChainlinkPriceFeedFork is Test {
17
+ // Chainlink feed addresses (Ethereum mainnet).
18
+ address constant ETH_USD_FEED = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419;
19
+ address constant BTC_USD_FEED = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c;
20
+
21
+ // Staleness threshold (1 hour).
22
+ uint256 constant THRESHOLD = 3600;
23
+
24
+ // Currency identifiers (arbitrary nonzero values for JBPrices mapping keys).
25
+ uint256 constant CURRENCY_ETH = 1;
26
+ uint256 constant CURRENCY_USD = 2;
27
+ uint256 constant CURRENCY_BTC = 3;
28
+
29
+ // Pinned block for reproducibility.
30
+ uint256 constant FORK_BLOCK = 22_000_000;
31
+
32
+ JBChainlinkV3PriceFeed ethUsdPriceFeed;
33
+ JBChainlinkV3PriceFeed btcUsdPriceFeed;
34
+ JBPrices prices;
35
+ address owner;
36
+
37
+ function setUp() public {
38
+ vm.createSelectFork("ethereum", FORK_BLOCK);
39
+
40
+ owner = makeAddr("owner");
41
+
42
+ // Deploy price feeds against real Chainlink aggregators.
43
+ ethUsdPriceFeed = new JBChainlinkV3PriceFeed(AggregatorV3Interface(ETH_USD_FEED), THRESHOLD);
44
+ btcUsdPriceFeed = new JBChainlinkV3PriceFeed(AggregatorV3Interface(BTC_USD_FEED), THRESHOLD);
45
+
46
+ // Deploy JBPrices with mock directory/permissions/projects (only price resolution is tested).
47
+ prices = new JBPrices(
48
+ IJBDirectory(makeAddr("directory")),
49
+ IJBPermissions(makeAddr("permissions")),
50
+ IJBProjects(makeAddr("projects")),
51
+ owner,
52
+ address(0)
53
+ );
54
+ }
55
+
56
+ // ------------------------------------------------------------------
57
+ // Live feed sanity checks
58
+ // ------------------------------------------------------------------
59
+
60
+ /// @notice Verify currentUnitPrice returns a sane ETH/USD price at the pinned block.
61
+ function test_currentUnitPrice_liveEthUsd() public view {
62
+ uint256 price18 = ethUsdPriceFeed.currentUnitPrice(18);
63
+
64
+ // ETH price should be between $500 and $50,000.
65
+ assertGt(price18, 500e18, "ETH price too low");
66
+ assertLt(price18, 50_000e18, "ETH price too high");
67
+
68
+ // Cross-check against raw latestRoundData.
69
+ (, int256 rawPrice,,,) = AggregatorV3Interface(ETH_USD_FEED).latestRoundData();
70
+ uint256 feedDecimals = AggregatorV3Interface(ETH_USD_FEED).decimals();
71
+ uint256 expected18 = uint256(rawPrice) * 10 ** (18 - feedDecimals);
72
+ assertEq(price18, expected18, "Price mismatch vs raw feed");
73
+ }
74
+
75
+ // ------------------------------------------------------------------
76
+ // Decimal scaling
77
+ // ------------------------------------------------------------------
78
+
79
+ /// @notice Verify correct scaling across 6, 8, 18, and 27 decimals.
80
+ function test_currentUnitPrice_differentDecimals() public view {
81
+ uint256 price6 = ethUsdPriceFeed.currentUnitPrice(6);
82
+ uint256 price8 = ethUsdPriceFeed.currentUnitPrice(8);
83
+ uint256 price18 = ethUsdPriceFeed.currentUnitPrice(18);
84
+ uint256 price27 = ethUsdPriceFeed.currentUnitPrice(27);
85
+
86
+ // Raw feed is 8 decimals — price8 should match it exactly.
87
+ (, int256 rawPrice,,,) = AggregatorV3Interface(ETH_USD_FEED).latestRoundData();
88
+ assertEq(price8, uint256(rawPrice), "8-decimal mismatch");
89
+
90
+ // 6 decimals = raw / 100 (truncated).
91
+ assertEq(price6, uint256(rawPrice) / 1e2, "6-decimal mismatch");
92
+
93
+ // 18 decimals = raw * 1e10.
94
+ assertEq(price18, uint256(rawPrice) * 1e10, "18-decimal mismatch");
95
+
96
+ // 27 decimals = raw * 1e19.
97
+ assertEq(price27, uint256(rawPrice) * 1e19, "27-decimal mismatch");
98
+ }
99
+
100
+ // ------------------------------------------------------------------
101
+ // JBPrices integration — direct feed
102
+ // ------------------------------------------------------------------
103
+
104
+ /// @notice Register ETH/USD feed and resolve via pricePerUnitOf.
105
+ function test_pricePerUnitOf_directFeed() public {
106
+ vm.prank(owner);
107
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
108
+
109
+ uint256 priceFromPrices = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
110
+ uint256 priceFromFeed = ethUsdPriceFeed.currentUnitPrice(18);
111
+
112
+ assertEq(priceFromPrices, priceFromFeed, "Direct feed mismatch");
113
+ }
114
+
115
+ // ------------------------------------------------------------------
116
+ // JBPrices integration — inverse feed
117
+ // ------------------------------------------------------------------
118
+
119
+ /// @notice Register ETH/USD feed but query USD/ETH — verify inverse calculation.
120
+ function test_pricePerUnitOf_inverseFeed() public {
121
+ vm.prank(owner);
122
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
123
+
124
+ uint256 inversePrice = prices.pricePerUnitOf(0, CURRENCY_ETH, CURRENCY_USD, 18);
125
+
126
+ // inverse = (1e18 * 1e18) / ethUsdPrice. For ETH ~$2000, inverse ~0.0005e18 = 5e14.
127
+ // Sane range: $500–$50k → inverse 2e13–2e15.
128
+ assertGt(inversePrice, 2e13, "Inverse price too low");
129
+ assertLt(inversePrice, 2e15, "Inverse price too high");
130
+
131
+ // Verify round-trip: price * inverse ≈ 1e18 (within mulDiv rounding).
132
+ uint256 directPrice = ethUsdPriceFeed.currentUnitPrice(18);
133
+ uint256 product = (directPrice * inversePrice) / 1e18;
134
+ // mulDiv truncation can cause up to ~1e4 wei error at these magnitudes.
135
+ assertApproxEqAbs(product, 1e18, 1e4, "Round-trip mismatch");
136
+ }
137
+
138
+ // ------------------------------------------------------------------
139
+ // JBPrices — same currency
140
+ // ------------------------------------------------------------------
141
+
142
+ /// @notice pricePerUnitOf(X, X, 18) should return 1e18 without any feed.
143
+ function test_pricePerUnitOf_sameCurrency() public view {
144
+ uint256 price = prices.pricePerUnitOf(0, CURRENCY_ETH, CURRENCY_ETH, 18);
145
+ assertEq(price, 1e18, "Same currency should be 1e18");
146
+ }
147
+
148
+ // ------------------------------------------------------------------
149
+ // JBPrices — default fallback
150
+ // ------------------------------------------------------------------
151
+
152
+ /// @notice Feed registered at projectId=0 should be used when querying projectId=99.
153
+ function test_pricePerUnitOf_defaultFallback() public {
154
+ vm.prank(owner);
155
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
156
+
157
+ uint256 defaultPrice = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
158
+ uint256 fallbackPrice = prices.pricePerUnitOf(99, CURRENCY_USD, CURRENCY_ETH, 18);
159
+
160
+ assertEq(fallbackPrice, defaultPrice, "Fallback should match default");
161
+ }
162
+
163
+ // ------------------------------------------------------------------
164
+ // Stale price revert
165
+ // ------------------------------------------------------------------
166
+
167
+ /// @notice Warping past the threshold should cause a StalePrice revert.
168
+ function test_stalePriceReverts() public {
169
+ // Snapshot updatedAt from the feed at this block.
170
+ (,,, uint256 updatedAt,) = AggregatorV3Interface(ETH_USD_FEED).latestRoundData();
171
+
172
+ // Warp far enough that the feed's updatedAt + threshold < block.timestamp.
173
+ uint256 warpTo = block.timestamp + 7200;
174
+ vm.warp(warpTo);
175
+
176
+ vm.expectRevert(
177
+ abi.encodeWithSelector(
178
+ JBChainlinkV3PriceFeed.JBChainlinkV3PriceFeed_StalePrice.selector, warpTo, THRESHOLD, updatedAt
179
+ )
180
+ );
181
+ ethUsdPriceFeed.currentUnitPrice(18);
182
+ }
183
+
184
+ // ------------------------------------------------------------------
185
+ // Feed immutability
186
+ // ------------------------------------------------------------------
187
+
188
+ /// @notice Registering the same currency pair twice should revert.
189
+ function test_feedImmutability() public {
190
+ vm.prank(owner);
191
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
192
+
193
+ vm.prank(owner);
194
+ vm.expectRevert(
195
+ abi.encodeWithSelector(
196
+ JBPrices.JBPrices_PriceFeedAlreadyExists.selector, IJBPriceFeed(address(ethUsdPriceFeed))
197
+ )
198
+ );
199
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(btcUsdPriceFeed)));
200
+ }
201
+
202
+ // ------------------------------------------------------------------
203
+ // Multiple feeds
204
+ // ------------------------------------------------------------------
205
+
206
+ /// @notice Register both ETH/USD and BTC/USD and verify independent resolution.
207
+ function test_multipleFeeds_ethUsd_btcUsd() public {
208
+ vm.startPrank(owner);
209
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
210
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_BTC, IJBPriceFeed(address(btcUsdPriceFeed)));
211
+ vm.stopPrank();
212
+
213
+ uint256 ethUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
214
+ uint256 btcUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_BTC, 18);
215
+
216
+ // ETH/USD: $500–$50,000.
217
+ assertGt(ethUsd, 500e18, "ETH/USD too low");
218
+ assertLt(ethUsd, 50_000e18, "ETH/USD too high");
219
+
220
+ // BTC/USD: $10,000–$500,000.
221
+ assertGt(btcUsd, 10_000e18, "BTC/USD too low");
222
+ assertLt(btcUsd, 500_000e18, "BTC/USD too high");
223
+
224
+ // BTC should be more expensive than ETH.
225
+ assertGt(btcUsd, ethUsd, "BTC should cost more than ETH");
226
+ }
227
+
228
+ // ------------------------------------------------------------------
229
+ // Cross-price derivation
230
+ // ------------------------------------------------------------------
231
+
232
+ /// @notice Derive ETH/BTC price from ETH/USD and BTC/USD feeds.
233
+ function test_crossPriceDerived() public {
234
+ vm.startPrank(owner);
235
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
236
+ prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_BTC, IJBPriceFeed(address(btcUsdPriceFeed)));
237
+ vm.stopPrank();
238
+
239
+ uint256 ethUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
240
+ uint256 btcUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_BTC, 18);
241
+
242
+ // ETH/BTC = ethUsd / btcUsd (how many BTC per 1 ETH).
243
+ uint256 ethPerBtc = (ethUsd * 1e18) / btcUsd;
244
+
245
+ // Should be between 0.01 and 0.1 (at 18 decimals: 1e16–1e17).
246
+ assertGt(ethPerBtc, 1e16, "ETH/BTC ratio too low");
247
+ assertLt(ethPerBtc, 1e17, "ETH/BTC ratio too high");
248
+ }
249
+ }
@@ -2,7 +2,6 @@
2
2
  pragma solidity ^0.8.17;
3
3
 
4
4
  import "forge-std/Test.sol";
5
- import {mulDiv} from "@prb/math/src/Common.sol";
6
5
  import {JBCashOuts} from "../../src/libraries/JBCashOuts.sol";
7
6
  import {JBFees} from "../../src/libraries/JBFees.sol";
8
7
  import {JBConstants} from "../../src/libraries/JBConstants.sol";
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {JBRuleset} from "src/structs/JBRuleset.sol";
5
- import {JBRulesetMetadata} from "src/structs/JBRulesetMetadata.sol";
6
- import {JBRulesetMetadataResolver} from "src/libraries/JBRulesetMetadataResolver.sol";
7
- import {JBConstants} from "src/libraries/JBConstants.sol";
8
- import {IJBRulesetApprovalHook} from "src/interfaces/IJBRulesetApprovalHook.sol";
4
+ import {JBRuleset} from "../../src/structs/JBRuleset.sol";
5
+ import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
6
+ import {JBRulesetMetadataResolver} from "../../src/libraries/JBRulesetMetadataResolver.sol";
7
+ import {JBConstants} from "../../src/libraries/JBConstants.sol";
8
+ import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
9
9
  import "lib/forge-std/src/Test.sol";
10
10
 
11
11
  contract JBTest is Test {
@@ -65,7 +65,7 @@ contract JBTest is Test {
65
65
  });
66
66
  }
67
67
 
68
- function generateEmptyMetadata() public view returns (JBRulesetMetadata memory) {
68
+ function generateEmptyMetadata() public pure returns (JBRulesetMetadata memory) {
69
69
  return JBRulesetMetadata({
70
70
  reservedPercent: 0,
71
71
  cashOutTaxRate: 0,
@@ -94,6 +94,7 @@ import {IPermit2, IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IPer
94
94
  import {DeployPermit2} from "@uniswap/permit2/test/utils/DeployPermit2.sol";
95
95
 
96
96
  import {MetadataResolverHelper} from "./MetadataResolverHelper.sol";
97
+ import {JBTest} from "./JBTest.sol";
97
98
 
98
99
  import {MockERC20} from "./../mock/MockERC20.sol";
99
100
 
@@ -102,7 +103,7 @@ import {mul as UD60x18mul, wrap as UD60x18wrap, unwrap as UD60x18unwrap} from "@
102
103
 
103
104
  // Base contract for Juicebox system tests.
104
105
  // Provides common functionality, such as deploying contracts on test setup.
105
- contract TestBaseWorkflow is Test, DeployPermit2 {
106
+ contract TestBaseWorkflow is JBTest, DeployPermit2 {
106
107
  // Multisig address used for testing.
107
108
  address private _multisig = address(123);
108
109
  address private _beneficiary = address(69_420);
@@ -6,8 +6,6 @@ import /* {*} from */ "../helpers/TestBaseWorkflow.sol";
6
6
  import {Phase3Handler} from "./handlers/Phase3Handler.sol";
7
7
  import {JBAccountingContext} from "../../src/structs/JBAccountingContext.sol";
8
8
  import {JBConstants} from "../../src/libraries/JBConstants.sol";
9
- import {JBFee} from "../../src/structs/JBFee.sol";
10
- import {JBSplitGroupIds} from "../../src/libraries/JBSplitGroupIds.sol";
11
9
 
12
10
  /// @title Phase3DeepInvariant
13
11
  /// @notice Multi-project deep invariant tests with strict equality checks.
@@ -362,7 +360,6 @@ contract Phase3DeepInvariant_Local is StdInvariant, TestBaseWorkflow {
362
360
  /// @notice After addToBalance(shouldReturn=true), returned fees must not exceed held fees.
363
361
  function invariant_P3_6_heldFeeReturnBounded() public view {
364
362
  uint256 returned = handler.ghost_totalReturnedFees(project2);
365
- uint256 heldTotal = handler.ghost_totalHeldFeeAmounts(project2);
366
363
 
367
364
  // Returned fees should never exceed what was held
368
365
  // (heldTotal may be 0 if we never tracked, so only check when both nonzero)
@@ -9,7 +9,6 @@ import {IJBMultiTerminal} from "../../../src/interfaces/IJBMultiTerminal.sol";
9
9
  import {IJBTerminalStore} from "../../../src/interfaces/IJBTerminalStore.sol";
10
10
  import {IJBController} from "../../../src/interfaces/IJBController.sol";
11
11
  import {IJBTokens} from "../../../src/interfaces/IJBTokens.sol";
12
- import {JBMultiTerminal} from "../../../src/JBMultiTerminal.sol";
13
12
 
14
13
  /// @title Phase3Handler
15
14
  /// @notice Stateful fuzzing handler with ghost variable tracking for strict invariant verification.
@@ -6,7 +6,6 @@ import {JBConstants} from "../../../src/libraries/JBConstants.sol";
6
6
  import {IJBMultiTerminal} from "../../../src/interfaces/IJBMultiTerminal.sol";
7
7
  import {IJBController} from "../../../src/interfaces/IJBController.sol";
8
8
  import {IJBTokens} from "../../../src/interfaces/IJBTokens.sol";
9
- import {IJBToken} from "../../../src/interfaces/IJBToken.sol";
10
9
 
11
10
  /// @notice Handler contract for JBTokens invariant testing.
12
11
  /// @dev Drives mint, burn, claim, and transfer operations; tracks holders for sum-of-balances checks.
@@ -22,7 +22,7 @@ contract ERC2771ForwarderMock is ERC2771Forwarder {
22
22
  return _hashTypedDataV4(
23
23
  keccak256(
24
24
  abi.encode(
25
- _FORWARD_REQUEST_TYPEHASH,
25
+ FORWARD_REQUEST_TYPEHASH,
26
26
  request.from,
27
27
  request.to,
28
28
  request.value,
@@ -3,7 +3,7 @@ pragma solidity 0.8.26;
3
3
 
4
4
  import /* {*} from */ "../helpers/TestBaseWorkflow.sol";
5
5
 
6
- import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7
7
 
8
8
  contract MockERC20 is ERC20 {
9
9
  function decimals() public view virtual override returns (uint8) {
@@ -3,7 +3,6 @@ pragma solidity 0.8.26;
3
3
 
4
4
  import /* {*} from */ "../helpers/TestBaseWorkflow.sol";
5
5
  import {IJBMultiTerminal} from "../../src/interfaces/IJBMultiTerminal.sol";
6
- import {JBMultiTerminal} from "../../src/JBMultiTerminal.sol";
7
6
  import {JBConstants} from "../../src/libraries/JBConstants.sol";
8
7
  import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
9
8
 
@@ -46,7 +46,7 @@ contract TestDeadlineFuzz_Local is JBTest {
46
46
  }
47
47
 
48
48
  /// @notice Ruleset queued too close to start returns Failed.
49
- function test_insufficientGap_isFailed() external {
49
+ function test_insufficientGap_isFailed() external view {
50
50
  uint48 start = uint48(block.timestamp + 1 days);
51
51
  uint48 queued = start - uint48(DURATION) + 1; // 1 second short of required gap
52
52
 
@@ -56,7 +56,7 @@ contract TestDeadlineFuzz_Local is JBTest {
56
56
  }
57
57
 
58
58
  /// @notice Ruleset with exactly enough gap and deadline not yet passed returns ApprovalExpected.
59
- function test_exactGap_deadlineNotPassed_isApprovalExpected() external {
59
+ function test_exactGap_deadlineNotPassed_isApprovalExpected() external view {
60
60
  // queue far enough in advance, and start is still far in the future
61
61
  uint48 start = uint48(block.timestamp + 2 * DURATION + 100);
62
62
  uint48 queued = start - uint48(DURATION);
@@ -67,7 +67,7 @@ contract TestDeadlineFuzz_Local is JBTest {
67
67
  }
68
68
 
69
69
  /// @notice Ruleset with enough gap and deadline passed returns Approved.
70
- function test_gapSufficient_deadlinePassed_isApproved() external {
70
+ function test_gapSufficient_deadlinePassed_isApproved() external view {
71
71
  uint48 start = uint48(block.timestamp + 1); // start is very soon
72
72
  uint48 queued = start - uint48(DURATION) - 1; // plenty of gap
73
73
 
@@ -7,7 +7,7 @@ import {JBMetadataResolver} from "../../../../src/libraries/JBMetadataResolver.s
7
7
 
8
8
  /// @notice Harness that exposes JBMetadataResolver internals and adds a combined operation
9
9
  /// to test memory corruption from _sliceBytes within a single execution context.
10
- contract M20M21Harness {
10
+ contract MetadataResolverEdgeCaseHarness {
11
11
  function createMetadata(bytes4[] memory ids, bytes[] memory datas) external pure returns (bytes memory) {
12
12
  return JBMetadataResolver.createMetadata(ids, datas);
13
13
  }
@@ -64,11 +64,11 @@ contract M20M21Harness {
64
64
  }
65
65
 
66
66
  /// @notice Tests for _sliceBytes over-copy and addToMetadata offset overflow.
67
- contract TestMetadataResolverM20M21 is JBTest {
68
- M20M21Harness harness;
67
+ contract TestMetadataResolverEdgeCases is JBTest {
68
+ MetadataResolverEdgeCaseHarness harness;
69
69
 
70
70
  function setUp() external {
71
- harness = new M20M21Harness();
71
+ harness = new MetadataResolverEdgeCaseHarness();
72
72
  }
73
73
 
74
74
  //*********************************************************************//
@@ -3,7 +3,6 @@ pragma solidity 0.8.26;
3
3
 
4
4
  import /* {*} from */ "../../../helpers/TestBaseWorkflow.sol";
5
5
  import {JBTest} from "../../../helpers/JBTest.sol";
6
- import {JBRulesetWeightCache} from "src/structs/JBRulesetWeightCache.sol";
7
6
 
8
7
  contract TestJBRulesetsUnits_Local is JBTest {
9
8
  // Contracts
@@ -53,7 +53,7 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
53
53
  override
54
54
  {}
55
55
 
56
- function migrateBalanceOf(uint256, address, IJBTerminal) external override returns (uint256) {
56
+ function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
57
57
  return 0;
58
58
  }
59
59