@bananapus/ownable-v6 0.0.7 → 0.0.9

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/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
@@ -212,7 +197,7 @@ interface IJBExample is IJBBase {
212
197
  | Public/external function | `camelCase` | `cashOutTokensOf` |
213
198
  | Internal/private function | `_camelCase` | `_processFee` |
214
199
  | Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
215
- | Function parameter | `camelCase` | `projectId`, `cashOutCount` |
200
+ | Function parameter | `camelCase` (no underscores) | `projectId`, `cashOutCount` |
216
201
 
217
202
  ## NatSpec
218
203
 
@@ -268,9 +253,12 @@ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
268
253
 
269
254
  ## Function Calls
270
255
 
271
- Use named parameters for readability when calling functions with 3+ arguments:
256
+ Use named arguments for all function calls with 2 or more arguments — in both `src/` and `script/`:
272
257
 
273
258
  ```solidity
259
+ // Good — named arguments
260
+ token.mint({account: beneficiary, amount: count});
261
+ _transferOwnership({newOwner: address(0), projectId: 0});
274
262
  PERMISSIONS.hasPermission({
275
263
  operator: sender,
276
264
  account: account,
@@ -279,8 +267,18 @@ PERMISSIONS.hasPermission({
279
267
  includeRoot: true,
280
268
  includeWildcardProjectId: true
281
269
  });
270
+
271
+ // Bad — positional arguments with 2+ args
272
+ token.mint(beneficiary, count);
273
+ _transferOwnership(address(0), 0);
282
274
  ```
283
275
 
276
+ Single-argument calls use positional style: `_burn(amount)`.
277
+
278
+ This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
279
+
280
+ Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
281
+
284
282
  ## Multiline Signatures
285
283
 
286
284
  ```solidity
@@ -334,9 +332,6 @@ optimizer_runs = 200
334
332
  libs = ["node_modules", "lib"]
335
333
  fs_permissions = [{ access = "read-write", path = "./"}]
336
334
 
337
- [profile.ci_sizes]
338
- optimizer_runs = 200
339
-
340
335
  [fuzz]
341
336
  runs = 4096
342
337
 
@@ -351,10 +346,14 @@ multiline_func_header = "all"
351
346
  wrap_comments = true
352
347
  ```
353
348
 
354
- **Variations:**
355
- - `evm_version = 'cancun'` for repos using transient storage (buyback-hook, router-terminal, univ4-router)
356
- - `via_ir = true` for repos hitting stack-too-deep (buyback-hook, banny-retail, univ4-lp-split-hook, deploy-all)
357
- - `optimizer = false` only for deploy-all-v6 (stack-too-deep with optimization)
349
+ **Optional sections (add only when needed):**
350
+ - `[rpc_endpoints]` repos with fork tests. Maps named endpoints to env vars (e.g. `ethereum = "${RPC_ETHEREUM_MAINNET}"`).
351
+ - `[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).
352
+
353
+ **Common variations:**
354
+ - `via_ir = true` when hitting stack-too-deep
355
+ - `optimizer = false` when optimization causes stack-too-deep
356
+ - `optimizer_runs` reduced when deep struct nesting causes stack-too-deep at 200 runs
358
357
 
359
358
  ### CI Workflows
360
359
 
@@ -384,8 +383,10 @@ jobs:
384
383
  uses: foundry-rs/foundry-toolchain@v1
385
384
  - name: Run tests
386
385
  run: forge test --fail-fast --summary --detailed --skip "*/script/**"
386
+ env:
387
+ RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
387
388
  - name: Check contract sizes
388
- run: FOUNDRY_PROFILE=ci_sizes forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
389
+ run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
389
390
  ```
390
391
 
391
392
  **lint.yml:**
@@ -407,11 +408,60 @@ jobs:
407
408
  run: forge fmt --check
408
409
  ```
409
410
 
411
+ **slither.yml** (repos with `src/` contracts only):
412
+ ```yaml
413
+ name: slither
414
+ on:
415
+ pull_request:
416
+ branches:
417
+ - main
418
+ push:
419
+ branches:
420
+ - main
421
+ jobs:
422
+ analyze:
423
+ runs-on: ubuntu-latest
424
+ steps:
425
+ - uses: actions/checkout@v4
426
+ with:
427
+ submodules: recursive
428
+ - uses: actions/setup-node@v4
429
+ with:
430
+ node-version: latest
431
+ - name: Install npm dependencies
432
+ run: npm install --omit=dev
433
+ - name: Install Foundry
434
+ uses: foundry-rs/foundry-toolchain@v1
435
+ - name: Run slither
436
+ uses: crytic/slither-action@v0.3.1
437
+ with:
438
+ slither-config: slither-ci.config.json
439
+ fail-on: medium
440
+ ```
441
+
442
+ **slither-ci.config.json:**
443
+ ```json
444
+ {
445
+ "detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
446
+ "exclude_informational": true,
447
+ "exclude_low": false,
448
+ "exclude_medium": false,
449
+ "exclude_high": false,
450
+ "disable_color": false,
451
+ "filter_paths": "(mocks/|test/|node_modules/|lib/)",
452
+ "legacy_ast": false
453
+ }
454
+ ```
455
+
456
+ **Variations:**
457
+ - 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.
458
+ - 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.
459
+
410
460
  ### package.json
411
461
 
412
462
  ```json
413
463
  {
414
- "name": "@bananapus/ownable-v6",
464
+ "name": "@bananapus/package-name-v6",
415
465
  "version": "x.x.x",
416
466
  "license": "MIT",
417
467
  "repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
@@ -431,13 +481,62 @@ jobs:
431
481
 
432
482
  ### remappings.txt
433
483
 
434
- Every repo has a `remappings.txt`. Minimal content:
484
+ Every repo has a `remappings.txt` as the **single source of truth** for import remappings. Never add remappings to `foundry.toml`.
485
+
486
+ **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.
487
+
488
+ **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.
489
+
490
+ **Minimal content** (most repos):
435
491
 
436
492
  ```
437
- @sphinx-labs/contracts/=lib/sphinx/packages/contracts/contracts/foundry
493
+ forge-std/=lib/forge-std/src/
438
494
  ```
439
495
 
440
- Additional mappings as needed for repo-specific dependencies.
496
+ Only add extra remappings for:
497
+ - **`forge-std`** — always needed (git submodule with `src/` subdirectory)
498
+ - **Repo-specific `lib/` submodules** that have no npm package (e.g., `hookmate/=lib/hookmate/src/`)
499
+ - **Symlinked npm packages** — need explicit `@scope/package/=node_modules/@scope/package/` entries
500
+ - **Nested transitive deps** — e.g., `@chainlink/contracts-ccip/` nested inside `@bananapus/suckers-v6/node_modules/`
501
+
502
+ **Never add remappings for:**
503
+ - npm packages that match their import path and are installed as real directories — they auto-resolve
504
+ - Short-form aliases (e.g., `@bananapus/core/` → `@bananapus/core-v6/src/`) — fix the import instead
505
+ - Packages available via npm that are also git submodules — remove the submodule, use npm
506
+
507
+ **Import path convention:**
508
+
509
+ | Package | Import path | Resolves to |
510
+ |---------|------------|-------------|
511
+ | `@bananapus/core-v6` | `@bananapus/core-v6/src/libraries/JBConstants.sol` | `node_modules/@bananapus/core-v6/src/...` |
512
+ | `@openzeppelin/contracts` | `@openzeppelin/contracts/token/ERC20/IERC20.sol` | `node_modules/@openzeppelin/contracts/...` |
513
+ | `@uniswap/v4-core` | `@uniswap/v4-core/src/interfaces/IPoolManager.sol` | `node_modules/@uniswap/v4-core/src/...` |
514
+
515
+ ### Linting
516
+
517
+ Solar (Foundry's built-in linter) runs automatically during `forge build`. It scans all `.sol` files in `libs` directories, including `node_modules`.
518
+
519
+ **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.
520
+
521
+ ### Fork Tests
522
+
523
+ 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.
524
+
525
+ ```solidity
526
+ function setUp() public {
527
+ vm.createSelectFork("ethereum");
528
+ // ... setup code
529
+ }
530
+ ```
531
+
532
+ The endpoint name (e.g. `"ethereum"`) maps to an env var via `foundry.toml`:
533
+
534
+ ```toml
535
+ [rpc_endpoints]
536
+ ethereum = "${RPC_ETHEREUM_MAINNET}"
537
+ ```
538
+
539
+ For multi-chain fork tests, add all needed endpoints.
441
540
 
442
541
  ### Formatting
443
542
 
@@ -465,4 +564,9 @@ CI checks formatting via `forge fmt --check`.
465
564
 
466
565
  ### Contract Size Checks
467
566
 
468
- 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.
567
+ 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`.
568
+
569
+
570
+ ## Repo-Specific Deviations
571
+
572
+ None. This repo follows the standard configuration exactly.
package/foundry.lock ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "lib/forge-std": {
3
+ "rev": "77876f8a5b44b770a935621bb331660c90ac928e"
4
+ }
5
+ }
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/ownable-v6",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,8 +10,9 @@
10
10
  "node": ">=20.0.0"
11
11
  },
12
12
  "dependencies": {
13
- "@bananapus/core-v6": "^0.0.11",
14
- "@openzeppelin/contracts": "^5.0.2"
13
+ "@bananapus/core-v6": "^0.0.16",
14
+ "@bananapus/permission-ids-v6": "^0.0.9",
15
+ "@openzeppelin/contracts": "^5.6.1"
15
16
  },
16
17
  "scripts": {
17
18
  "test": "forge test",
@@ -75,7 +75,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
75
75
  revert JBOwnableOverrides_InvalidNewOwner();
76
76
  }
77
77
 
78
- _transferOwnership(initialOwner, initialProjectIdOwner);
78
+ _transferOwnership({newOwner: initialOwner, projectId: initialProjectIdOwner});
79
79
  }
80
80
 
81
81
  //*********************************************************************//
@@ -140,7 +140,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
140
140
  /// @dev This can only be called by the current owner.
141
141
  function renounceOwnership() public virtual override {
142
142
  _checkOwner();
143
- _transferOwnership(address(0), 0);
143
+ _transferOwnership({newOwner: address(0), projectId: 0});
144
144
  }
145
145
 
146
146
  /// @notice Sets the permission ID the owner can use to give other addresses owner access.
@@ -162,7 +162,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
162
162
  revert JBOwnableOverrides_InvalidNewOwner();
163
163
  }
164
164
 
165
- _transferOwnership(newOwner, 0);
165
+ _transferOwnership({newOwner: newOwner, projectId: 0});
166
166
  }
167
167
 
168
168
  /// @notice Transfer ownership of this contract to a new Juicebox project.
@@ -181,7 +181,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
181
181
  revert JBOwnableOverrides_ProjectDoesNotExist();
182
182
  }
183
183
 
184
- _transferOwnership(address(0), uint88(projectId));
184
+ // forge-lint: disable-next-line(unsafe-typecast)
185
+ _transferOwnership({newOwner: address(0), projectId: uint88(projectId)});
185
186
  }
186
187
 
187
188
  //*********************************************************************//
@@ -207,7 +208,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
207
208
  /// @notice Helper to allow for drop-in replacement of OpenZeppelin `Ownable`.
208
209
  /// @param newOwner The address that should receive ownership of this contract.
209
210
  function _transferOwnership(address newOwner) internal virtual {
210
- _transferOwnership(newOwner, 0);
211
+ _transferOwnership({newOwner: newOwner, projectId: 0});
211
212
  }
212
213
 
213
214
  /// @notice Transfers this contract's ownership to an address (`newOwner`) OR a Juicebox project (`projectId`).
@@ -238,6 +239,6 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
238
239
  // This is to prevent permissions clashes for the new user/owner.
239
240
  jbOwner = JBOwner({owner: newOwner, projectId: projectId, permissionId: 0});
240
241
  // Emit a transfer event with the new owner's address.
241
- _emitTransferEvent(oldOwner, newOwner, projectId);
242
+ _emitTransferEvent({previousOwner: oldOwner, newOwner: newOwner, newProjectId: projectId});
242
243
  }
243
244
  }
@@ -7,6 +7,7 @@ pragma solidity ^0.8.0;
7
7
  /// `owner` address has owner access.
8
8
  /// @custom:member permissionId The permission ID which corresponds to owner access. See `JBPermissions` in `nana-core`
9
9
  /// and `nana-permission-ids`.
10
+ // forge-lint: disable-next-line(pascal-case-struct)
10
11
  struct JBOwner {
11
12
  address owner;
12
13
  uint88 projectId;
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: UNLICENSED
2
2
  pragma solidity ^0.8.26;
3
3
 
4
- import "forge-std/Test.sol";
4
+ import {Test} from "forge-std/Test.sol";
5
5
  import {MockOwnable} from "./mocks/MockOwnable.sol";
6
6
  import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
7
7
 
@@ -13,8 +13,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
13
13
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
14
14
 
15
15
  contract OwnableTest is Test {
16
- IJBProjects PROJECTS;
17
- IJBPermissions PERMISSIONS;
16
+ IJBProjects projects;
17
+ IJBPermissions permissions;
18
18
 
19
19
  modifier isNotContract(address a) {
20
20
  uint256 size;
@@ -27,9 +27,9 @@ contract OwnableTest is Test {
27
27
 
28
28
  function setUp() public {
29
29
  // Deploy the permissions contract.
30
- PERMISSIONS = new JBPermissions(address(0));
30
+ permissions = new JBPermissions(address(0));
31
31
  // Deploy the projects contract.
32
- PROJECTS = new JBProjects(address(123), address(0), address(0));
32
+ projects = new JBProjects(address(123), address(0), address(0));
33
33
  }
34
34
 
35
35
  function testDeployerDoesNotBecomeOwner(address deployer, address owner) public isNotContract(owner) {
@@ -37,7 +37,7 @@ contract OwnableTest is Test {
37
37
  vm.assume(owner != address(0));
38
38
 
39
39
  vm.prank(deployer);
40
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, uint88(0));
40
+ MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
41
41
 
42
42
  assertEq(owner, ownable.owner(), "Deployer did not become the owner.");
43
43
  }
@@ -56,17 +56,18 @@ contract OwnableTest is Test {
56
56
  vm.assume(newProjectOwner != address(0));
57
57
 
58
58
  // Create a project for the owner.
59
- uint256 projectId = PROJECTS.createFor(projectOwner);
59
+ uint256 projectId = projects.createFor(projectOwner);
60
60
 
61
61
  // Create the `Ownable` contract.
62
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
62
+ // forge-lint: disable-next-line(unsafe-typecast)
63
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
63
64
 
64
65
  // Make sure the deployer owns it.
65
66
  assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
66
67
 
67
68
  // Transfer the project's ownership.
68
69
  vm.prank(projectOwner);
69
- PROJECTS.transferFrom(projectOwner, newProjectOwner, projectId);
70
+ projects.transferFrom(projectOwner, newProjectOwner, projectId);
70
71
 
71
72
  // Make sure the `Ownable` contract has also been transferred to the new project owner.
72
73
  assertEq(newProjectOwner, ownable.owner(), "Ownable did not follow the Project owner.");
@@ -86,10 +87,11 @@ contract OwnableTest is Test {
86
87
  vm.assume(projectOwner != address(0));
87
88
 
88
89
  // Create a project for the owner.
89
- uint256 _projectId = PROJECTS.createFor(projectOwner);
90
+ uint256 _projectId = projects.createFor(projectOwner);
90
91
 
91
92
  // Create the `Ownable` contract.
92
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
93
+ // forge-lint: disable-next-line(unsafe-typecast)
94
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
93
95
 
94
96
  // Make sure the project owner owns it.
95
97
  assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
@@ -100,7 +102,7 @@ contract OwnableTest is Test {
100
102
  // Make sure it was transferred to the new owner.
101
103
  assertEq(newOwnableOwner, ownable.owner());
102
104
  // Sanity check to make sure it only the `Ownable` changed, and that the project did not.
103
- assertEq(PROJECTS.ownerOf(_projectId), projectOwner);
105
+ assertEq(projects.ownerOf(_projectId), projectOwner);
104
106
  }
105
107
 
106
108
  function testCantTransferToProjectZero(address owner) public {
@@ -108,7 +110,7 @@ contract OwnableTest is Test {
108
110
  vm.startPrank(owner);
109
111
 
110
112
  // Create the `Ownable` contract.
111
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, 0);
113
+ MockOwnable ownable = new MockOwnable(projects, permissions, owner, 0);
112
114
 
113
115
  vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
114
116
 
@@ -122,7 +124,7 @@ contract OwnableTest is Test {
122
124
  vm.startPrank(owner);
123
125
 
124
126
  // Create the `Ownable` contract.
125
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, uint88(0));
127
+ MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
126
128
 
127
129
  vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
128
130
 
@@ -145,18 +147,19 @@ contract OwnableTest is Test {
145
147
  vm.assume(newProjectOwner != address(0));
146
148
 
147
149
  // Create a project for the owner.
148
- uint256 _projectId = PROJECTS.createFor(projectOwner);
150
+ uint256 _projectId = projects.createFor(projectOwner);
149
151
 
150
152
  // Create the `Ownable` contract.
151
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
153
+ // forge-lint: disable-next-line(unsafe-typecast)
154
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
152
155
 
153
156
  // Make sure the project owner owns it.
154
157
  assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
155
158
 
156
159
  // Transfer the project ownership.
157
160
  vm.prank(projectOwner);
158
- PROJECTS.transferFrom(projectOwner, newProjectOwner, _projectId);
159
- assertEq(PROJECTS.ownerOf(_projectId), newProjectOwner);
161
+ projects.transferFrom(projectOwner, newProjectOwner, _projectId);
162
+ assertEq(projects.ownerOf(_projectId), newProjectOwner);
160
163
 
161
164
  // Make sure the `Ownable` contract has also been transferred to the new project owner.
162
165
  assertEq(newProjectOwner, ownable.owner());
@@ -167,7 +170,7 @@ contract OwnableTest is Test {
167
170
  vm.assume(deployer != owner);
168
171
 
169
172
  // Create the `Ownable` contract.
170
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, uint88(0));
173
+ MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
171
174
 
172
175
  // Transfer ownership to the project owner.
173
176
  vm.prank(owner);
@@ -185,11 +188,12 @@ contract OwnableTest is Test {
185
188
  vm.assume(projectOwner != address(0));
186
189
 
187
190
  // Create a project for the owner.
188
- uint256 _projectId = PROJECTS.createFor(projectOwner);
191
+ uint256 _projectId = projects.createFor(projectOwner);
189
192
 
190
193
  // Create the `Ownable` contract.
191
194
  vm.prank(deployer);
192
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
195
+ // forge-lint: disable-next-line(unsafe-typecast)
196
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
193
197
 
194
198
  // Renounce the ownership.
195
199
  vm.prank(projectOwner);
@@ -218,10 +222,11 @@ contract OwnableTest is Test {
218
222
  }
219
223
 
220
224
  // Create a project for the owner.
221
- uint256 _projectId = PROJECTS.createFor(projectOwner);
225
+ uint256 _projectId = projects.createFor(projectOwner);
222
226
 
223
227
  // Create the `Ownable` contract.
224
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
228
+ // forge-lint: disable-next-line(unsafe-typecast)
229
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
225
230
 
226
231
  // Set the required permission.
227
232
  vm.prank(projectOwner);
@@ -254,8 +259,9 @@ contract OwnableTest is Test {
254
259
 
255
260
  // The owner gives permission to the caller.
256
261
  vm.prank(projectOwner);
257
- PERMISSIONS.setPermissionsFor(
262
+ permissions.setPermissionsFor(
258
263
  projectOwner,
264
+ // forge-lint: disable-next-line(unsafe-typecast)
259
265
  JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
260
266
  );
261
267
 
@@ -296,10 +302,11 @@ contract OwnableTest is Test {
296
302
  }
297
303
 
298
304
  // Create a project for the owner.
299
- uint256 _projectId = PROJECTS.createFor(projectOwner);
305
+ uint256 _projectId = projects.createFor(projectOwner);
300
306
 
301
307
  // Create the `Ownable` contract.
302
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
308
+ // forge-lint: disable-next-line(unsafe-typecast)
309
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
303
310
 
304
311
  // Set the permission that is required.
305
312
  ownable.setPermission(requiredPermissionId);
@@ -331,8 +338,9 @@ contract OwnableTest is Test {
331
338
 
332
339
  // The owner gives permission to the caller.
333
340
  vm.prank(projectOwner);
334
- PERMISSIONS.setPermissionsFor(
341
+ permissions.setPermissionsFor(
335
342
  projectOwner,
343
+ // forge-lint: disable-next-line(unsafe-typecast)
336
344
  JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
337
345
  );
338
346
 
@@ -356,19 +364,20 @@ contract OwnableTest is Test {
356
364
  vm.assume(owner != address(0) && projectOwner != address(0));
357
365
 
358
366
  // Create a project for the owner.
359
- uint256 _projectId = PROJECTS.createFor(projectOwner);
367
+ uint256 _projectId = projects.createFor(projectOwner);
360
368
 
361
369
  // Should revert because we set both a owner and a projectOwner
362
370
  vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
363
371
 
364
372
  // Create the `Ownable` contract.
365
- new MockOwnable(PROJECTS, PERMISSIONS, address(owner), uint88(_projectId));
373
+ // forge-lint: disable-next-line(unsafe-typecast)
374
+ new MockOwnable(projects, permissions, address(owner), uint88(_projectId));
366
375
  }
367
376
 
368
377
  function testCantInitializeAsRenounced() public {
369
378
  // Should revert because we set both a owner and a projectOwner
370
379
  vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
371
380
  // Create the `Ownable` contract.
372
- new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(0));
381
+ new MockOwnable(projects, permissions, address(0), uint88(0));
373
382
  }
374
383
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: UNLICENSED
2
2
  pragma solidity ^0.8.26;
3
3
 
4
- import "forge-std/Test.sol";
4
+ import {Test} from "forge-std/Test.sol";
5
5
  import {MockOwnable} from "./mocks/MockOwnable.sol";
6
6
  import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
7
7
 
@@ -16,8 +16,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
16
16
  /// @notice Adversarial security tests for JBOwnable covering edge cases
17
17
  /// around dual ownership, permission semantics, and renounced contracts.
18
18
  contract OwnableAttacks is Test {
19
- IJBProjects PROJECTS;
20
- IJBPermissions PERMISSIONS;
19
+ IJBProjects projects;
20
+ IJBPermissions permissions;
21
21
 
22
22
  address alice = makeAddr("alice");
23
23
  address bob = makeAddr("bob");
@@ -33,25 +33,26 @@ contract OwnableAttacks is Test {
33
33
  }
34
34
 
35
35
  function setUp() public {
36
- PERMISSIONS = new JBPermissions(address(0));
37
- PROJECTS = new JBProjects(address(123), address(0), address(0));
36
+ permissions = new JBPermissions(address(0));
37
+ projects = new JBProjects(address(123), address(0), address(0));
38
38
  }
39
39
 
40
40
  // =========================================================================
41
41
  // Test 1: Constructor rejects both owner AND projectId set
42
42
  // =========================================================================
43
43
  function test_bothOwnerAndProjectId_constructorReverts() public {
44
- uint256 projectId = PROJECTS.createFor(alice);
44
+ uint256 projectId = projects.createFor(alice);
45
45
 
46
46
  vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
47
- new MockOwnable(PROJECTS, PERMISSIONS, bob, uint88(projectId));
47
+ // forge-lint: disable-next-line(unsafe-typecast)
48
+ new MockOwnable(projects, permissions, bob, uint88(projectId));
48
49
  }
49
50
 
50
51
  // =========================================================================
51
52
  // Test 2: Renounced contract — protectedMethod always reverts
52
53
  // =========================================================================
53
54
  function test_renounced_protectedMethodAlwaysReverts() public {
54
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
55
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
55
56
 
56
57
  // Owner can call.
57
58
  vm.prank(alice);
@@ -82,8 +83,9 @@ contract OwnableAttacks is Test {
82
83
  /// @notice After any ownership transfer, permissionId should reset to 0.
83
84
  /// This prevents stale permission delegation.
84
85
  function test_permissionIdResetOnTransfer() public {
85
- uint256 projectId = PROJECTS.createFor(alice);
86
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
86
+ uint256 projectId = projects.createFor(alice);
87
+ // forge-lint: disable-next-line(unsafe-typecast)
88
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
87
89
 
88
90
  // Set permission ID.
89
91
  vm.prank(alice);
@@ -105,8 +107,9 @@ contract OwnableAttacks is Test {
105
107
  // =========================================================================
106
108
  /// @notice After transferring project NFT, old owner should lose access.
107
109
  function test_staleOwner_afterNFTTransfer() public {
108
- uint256 projectId = PROJECTS.createFor(alice);
109
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
110
+ uint256 projectId = projects.createFor(alice);
111
+ // forge-lint: disable-next-line(unsafe-typecast)
112
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
110
113
 
111
114
  // Alice is current owner.
112
115
  assertEq(ownable.owner(), alice);
@@ -115,7 +118,7 @@ contract OwnableAttacks is Test {
115
118
 
116
119
  // Transfer project NFT to bob.
117
120
  vm.prank(alice);
118
- PROJECTS.transferFrom(alice, bob, projectId);
121
+ projects.transferFrom(alice, bob, projectId);
119
122
 
120
123
  // Alice should no longer be owner.
121
124
  assertEq(ownable.owner(), bob, "Bob should be new owner");
@@ -135,7 +138,7 @@ contract OwnableAttacks is Test {
135
138
  // =========================================================================
136
139
  /// @notice transferOwnershipToProject with projectId > type(uint88).max should revert.
137
140
  function test_transferOwnershipToProject_overflowReverts() public {
138
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
141
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
139
142
 
140
143
  // type(uint88).max + 1 = 309485009821345068724781056
141
144
  uint256 overflowId = uint256(type(uint88).max) + 1;
@@ -152,11 +155,12 @@ contract OwnableAttacks is Test {
152
155
  /// doesn't grant access to a different project's JBOwnable.
153
156
  function test_rootOnWrongProject_noAccess() public {
154
157
  // Create two projects.
155
- uint256 aliceProject = PROJECTS.createFor(alice);
156
- uint256 attackerProject = PROJECTS.createFor(attacker);
158
+ uint256 aliceProject = projects.createFor(alice);
159
+ uint256 attackerProject = projects.createFor(attacker);
157
160
 
158
161
  // Ownable is owned by alice's project.
159
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(aliceProject));
162
+ // forge-lint: disable-next-line(unsafe-typecast)
163
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(aliceProject));
160
164
 
161
165
  // Set permission ID so delegated access is possible.
162
166
  vm.prank(alice);
@@ -167,8 +171,9 @@ contract OwnableAttacks is Test {
167
171
  rootPerms[0] = 1; // ROOT
168
172
 
169
173
  vm.prank(attacker);
170
- PERMISSIONS.setPermissionsFor(
174
+ permissions.setPermissionsFor(
171
175
  attacker,
176
+ // forge-lint: disable-next-line(unsafe-typecast)
172
177
  JBPermissionsData({operator: attacker, projectId: uint56(attackerProject), permissionIds: rootPerms})
173
178
  );
174
179
 
@@ -1,14 +1,12 @@
1
1
  // SPDX-License-Identifier: UNLICENSED
2
2
  pragma solidity ^0.8.26;
3
3
 
4
- import "forge-std/Test.sol";
4
+ import {Test} from "forge-std/Test.sol";
5
5
  import {MockOwnable} from "./mocks/MockOwnable.sol";
6
6
  import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
7
- import {JBOwner} from "../src/structs/JBOwner.sol";
8
7
  import {IJBOwnable} from "../src/interfaces/IJBOwnable.sol";
9
8
 
10
9
  import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
11
- import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
12
10
  import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
13
11
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
14
12
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
@@ -18,8 +16,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
18
16
  /// @notice Edge case and gap tests for JBOwnable: multi-hop NFT transfers,
19
17
  /// project-to-project ownership, permissionId lifecycle, and nonexistent projects.
20
18
  contract OwnableEdgeCases is Test {
21
- IJBProjects PROJECTS;
22
- IJBPermissions PERMISSIONS;
19
+ IJBProjects projects;
20
+ IJBPermissions permissions;
23
21
 
24
22
  address alice = makeAddr("alice");
25
23
  address bob = makeAddr("bob");
@@ -36,32 +34,33 @@ contract OwnableEdgeCases is Test {
36
34
  }
37
35
 
38
36
  function setUp() public {
39
- PERMISSIONS = new JBPermissions(address(0));
40
- PROJECTS = new JBProjects(address(123), address(0), address(0));
37
+ permissions = new JBPermissions(address(0));
38
+ projects = new JBProjects(address(123), address(0), address(0));
41
39
  }
42
40
 
43
41
  // =========================================================================
44
42
  // Test 1: Multi-hop NFT transfer — ownership follows through A→B→C→D
45
43
  // =========================================================================
46
44
  function test_multiHopNFTTransfer_ownerFollows() public {
47
- uint256 projectId = PROJECTS.createFor(alice);
48
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
45
+ uint256 projectId = projects.createFor(alice);
46
+ // forge-lint: disable-next-line(unsafe-typecast)
47
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
49
48
 
50
49
  assertEq(ownable.owner(), alice);
51
50
 
52
51
  // Transfer NFT: alice → bob
53
52
  vm.prank(alice);
54
- PROJECTS.transferFrom(alice, bob, projectId);
53
+ projects.transferFrom(alice, bob, projectId);
55
54
  assertEq(ownable.owner(), bob, "Should follow to bob");
56
55
 
57
56
  // Transfer NFT: bob → charlie
58
57
  vm.prank(bob);
59
- PROJECTS.transferFrom(bob, charlie, projectId);
58
+ projects.transferFrom(bob, charlie, projectId);
60
59
  assertEq(ownable.owner(), charlie, "Should follow to charlie");
61
60
 
62
61
  // Transfer NFT: charlie → dave
63
62
  vm.prank(charlie);
64
- PROJECTS.transferFrom(charlie, dave, projectId);
63
+ projects.transferFrom(charlie, dave, projectId);
65
64
  assertEq(ownable.owner(), dave, "Should follow to dave");
66
65
 
67
66
  // dave can call protectedMethod, alice/bob/charlie cannot
@@ -85,10 +84,11 @@ contract OwnableEdgeCases is Test {
85
84
  // Test 2: Transfer project → different project
86
85
  // =========================================================================
87
86
  function test_transferProjectToProject() public {
88
- uint256 projectA = PROJECTS.createFor(alice);
89
- uint256 projectB = PROJECTS.createFor(bob);
87
+ uint256 projectA = projects.createFor(alice);
88
+ uint256 projectB = projects.createFor(bob);
90
89
 
91
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectA));
90
+ // forge-lint: disable-next-line(unsafe-typecast)
91
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
92
92
  assertEq(ownable.owner(), alice);
93
93
 
94
94
  // Transfer ownership from project A to project B.
@@ -112,11 +112,11 @@ contract OwnableEdgeCases is Test {
112
112
  // Test 3: Full ownership cycle: address → project → address → project
113
113
  // =========================================================================
114
114
  function test_fullOwnershipCycle() public {
115
- uint256 projectA = PROJECTS.createFor(alice);
116
- uint256 projectB = PROJECTS.createFor(bob);
115
+ uint256 projectA = projects.createFor(alice);
116
+ uint256 projectB = projects.createFor(bob);
117
117
 
118
118
  // Start with address ownership.
119
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, charlie, 0);
119
+ MockOwnable ownable = new MockOwnable(projects, permissions, charlie, 0);
120
120
  assertEq(ownable.owner(), charlie);
121
121
 
122
122
  // charlie → project A (alice)
@@ -137,6 +137,7 @@ contract OwnableEdgeCases is Test {
137
137
  // Verify jbOwner struct is correct (projectId set, owner zeroed).
138
138
  (address storedOwner, uint88 storedProjectId, uint8 storedPermId) = ownable.jbOwner();
139
139
  assertEq(storedOwner, address(0), "owner field should be zero in project mode");
140
+ // forge-lint: disable-next-line(unsafe-typecast)
140
141
  assertEq(storedProjectId, uint88(projectB), "projectId should be projectB");
141
142
  assertEq(storedPermId, 0, "permissionId should be 0");
142
143
  }
@@ -145,9 +146,10 @@ contract OwnableEdgeCases is Test {
145
146
  // Test 4: permissionId lifecycle through multiple transfers
146
147
  // =========================================================================
147
148
  function test_permissionIdLifecycle() public {
148
- uint256 projectA = PROJECTS.createFor(alice);
149
+ uint256 projectA = projects.createFor(alice);
149
150
 
150
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectA));
151
+ // forge-lint: disable-next-line(unsafe-typecast)
152
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
151
153
 
152
154
  // Set permissionId to 42.
153
155
  vm.prank(alice);
@@ -192,7 +194,7 @@ contract OwnableEdgeCases is Test {
192
194
  function test_nonOwnerCannotSetPermissionId(address nonOwner) public {
193
195
  vm.assume(nonOwner != alice && nonOwner != address(0));
194
196
 
195
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
197
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
196
198
 
197
199
  vm.prank(nonOwner);
198
200
  vm.expectRevert();
@@ -204,8 +206,8 @@ contract OwnableEdgeCases is Test {
204
206
  // =========================================================================
205
207
  function test_transferToNonexistentProject_reverts() public {
206
208
  // Create one project so count == 1.
207
- PROJECTS.createFor(alice);
208
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
209
+ projects.createFor(alice);
210
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
209
211
 
210
212
  // Project 2 doesn't exist.
211
213
  vm.prank(alice);
@@ -223,8 +225,9 @@ contract OwnableEdgeCases is Test {
223
225
  // transferred, old delegate loses access
224
226
  // =========================================================================
225
227
  function test_delegatedAccess_lostAfterNFTTransfer() public {
226
- uint256 projectId = PROJECTS.createFor(alice);
227
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
228
+ uint256 projectId = projects.createFor(alice);
229
+ // forge-lint: disable-next-line(unsafe-typecast)
230
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
228
231
 
229
232
  // Set permissionId so delegation is possible.
230
233
  vm.prank(alice);
@@ -234,8 +237,10 @@ contract OwnableEdgeCases is Test {
234
237
  uint8[] memory permIds = new uint8[](1);
235
238
  permIds[0] = 42;
236
239
  vm.prank(alice);
237
- PERMISSIONS.setPermissionsFor(
238
- alice, JBPermissionsData({operator: charlie, projectId: uint56(projectId), permissionIds: permIds})
240
+ permissions.setPermissionsFor(
241
+ // forge-lint: disable-next-line(unsafe-typecast)
242
+ alice,
243
+ JBPermissionsData({operator: charlie, projectId: uint56(projectId), permissionIds: permIds})
239
244
  );
240
245
 
241
246
  // Charlie can call protectedMethod (delegated via permissions).
@@ -244,7 +249,7 @@ contract OwnableEdgeCases is Test {
244
249
 
245
250
  // Transfer NFT to bob.
246
251
  vm.prank(alice);
247
- PROJECTS.transferFrom(alice, bob, projectId);
252
+ projects.transferFrom(alice, bob, projectId);
248
253
 
249
254
  // Charlie's delegation was from alice. Now owner is bob.
250
255
  // Charlie should lose access because _checkOwner resolves to bob,
@@ -262,7 +267,7 @@ contract OwnableEdgeCases is Test {
262
267
  // Test 8: OwnershipTransferred event emitted correctly
263
268
  // =========================================================================
264
269
  function test_ownershipTransferredEvent() public {
265
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
270
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
266
271
 
267
272
  // Transfer to bob — expect event.
268
273
  vm.expectEmit(true, true, false, true);
@@ -276,7 +281,7 @@ contract OwnableEdgeCases is Test {
276
281
  // Test 9: PermissionIdChanged event emitted correctly
277
282
  // =========================================================================
278
283
  function test_permissionIdChangedEvent() public {
279
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
284
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
280
285
 
281
286
  vm.expectEmit(true, true, false, true);
282
287
  emit IJBOwnable.PermissionIdChanged(42, alice);
@@ -291,8 +296,8 @@ contract OwnableEdgeCases is Test {
291
296
  function testFuzz_transferToProject(address projectOwner) public isNotContract(projectOwner) {
292
297
  vm.assume(projectOwner != address(0));
293
298
 
294
- uint256 projectId = PROJECTS.createFor(projectOwner);
295
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
299
+ uint256 projectId = projects.createFor(projectOwner);
300
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
296
301
 
297
302
  vm.prank(alice);
298
303
  ownable.transferOwnershipToProject(projectId);
@@ -302,6 +307,7 @@ contract OwnableEdgeCases is Test {
302
307
  // Verify jbOwner struct.
303
308
  (address storedOwner, uint88 storedProjectId,) = ownable.jbOwner();
304
309
  assertEq(storedOwner, address(0), "stored owner should be zero");
310
+ // forge-lint: disable-next-line(unsafe-typecast)
305
311
  assertEq(storedProjectId, uint88(projectId), "stored projectId should match");
306
312
  }
307
313
 
@@ -311,8 +317,8 @@ contract OwnableEdgeCases is Test {
311
317
  /// @notice After renouncing, no one can call transferOwnership, transferOwnershipToProject,
312
318
  /// setPermissionId, or renounceOwnership again.
313
319
  function test_renouncedContract_cannotReclaim() public {
314
- uint256 projectId = PROJECTS.createFor(alice);
315
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
320
+ uint256 projectId = projects.createFor(alice);
321
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
316
322
 
317
323
  vm.prank(alice);
318
324
  ownable.renounceOwnership();
@@ -349,7 +355,7 @@ contract OwnableEdgeCases is Test {
349
355
  /// NOT ERC2771Context. This test documents that a trusted forwarder
350
356
  /// appending a sender address to calldata does NOT affect _checkOwner.
351
357
  function test_noERC2771_trustedForwarderHasNoEffect() public {
352
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
358
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
353
359
 
354
360
  // Simulate what a trusted forwarder would do: call with alice's address
355
361
  // appended to calldata. Since JBOwnable uses plain Context, this has no effect.
@@ -4,15 +4,6 @@ pragma solidity ^0.8.26;
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {OwnableHandler} from "./handlers/OwnableHandler.sol";
6
6
 
7
- import {MockOwnable} from "./mocks/MockOwnable.sol";
8
- import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
9
- import {JBOwner} from "../src/structs/JBOwner.sol";
10
- import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
11
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
12
- import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
13
- import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
14
- import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
15
-
16
7
  contract OwnableInvariantTests is Test {
17
8
  OwnableHandler handler;
18
9
 
@@ -5,12 +5,9 @@ pragma solidity ^0.8.26;
5
5
  import {CommonBase} from "forge-std/Base.sol";
6
6
  import {StdCheats} from "forge-std/StdCheats.sol";
7
7
  import {StdUtils} from "forge-std/StdUtils.sol";
8
- import {console} from "forge-std/console.sol";
9
-
10
- import {MockOwnable, JBOwnableOverrides} from "../mocks/MockOwnable.sol";
8
+ import {MockOwnable} from "../mocks/MockOwnable.sol";
11
9
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
12
10
  import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
13
- import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
14
11
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
15
12
  import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
16
13
 
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: UNLICENSED
2
2
  pragma solidity ^0.8.26;
3
3
 
4
- import {JBOwnable, JBOwnableOverrides} from "../../src/JBOwnable.sol";
4
+ import {JBOwnable} from "../../src/JBOwnable.sol";
5
5
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
6
6
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
7
7
 
@@ -3,7 +3,6 @@ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {MockOwnable} from "../mocks/MockOwnable.sol";
6
- import {JBOwnableOverrides} from "../../src/JBOwnableOverrides.sol";
7
6
 
8
7
  import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
9
8
  import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
@@ -11,34 +10,35 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
11
10
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
12
11
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
13
12
 
14
- /// @title L65_BurnLockProtection
13
+ /// @title BurnLockProtection
15
14
  /// @notice Verifies that if a project NFT is burned/invalidated,
16
15
  /// owner() returns address(0) and _checkOwner() reverts gracefully instead of
17
16
  /// permanently locking the contract with an unrecoverable revert.
18
- contract L65_BurnLockProtection is Test {
19
- IJBProjects PROJECTS;
20
- IJBPermissions PERMISSIONS;
17
+ contract BurnLockProtection is Test {
18
+ IJBProjects projects;
19
+ IJBPermissions permissions;
21
20
 
22
21
  address alice = makeAddr("alice");
23
22
  address bob = makeAddr("bob");
24
23
 
25
24
  function setUp() public {
26
- PERMISSIONS = new JBPermissions(address(0));
27
- PROJECTS = new JBProjects(address(123), address(0), address(0));
25
+ permissions = new JBPermissions(address(0));
26
+ projects = new JBProjects(address(123), address(0), address(0));
28
27
  }
29
28
 
30
29
  /// @notice When a project NFT is burned (simulated via mockCallRevert), owner() should
31
30
  /// return address(0) instead of reverting — contract degrades to "renounced" state.
32
31
  function test_burnedProjectNFT_ownerReturnsZero() public {
33
- uint256 projectId = PROJECTS.createFor(alice);
34
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
32
+ uint256 projectId = projects.createFor(alice);
33
+ // forge-lint: disable-next-line(unsafe-typecast)
34
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
35
35
 
36
36
  // Verify normal operation first.
37
37
  assertEq(ownable.owner(), alice, "Owner should be alice before burn");
38
38
 
39
39
  // Simulate project NFT burn by making ownerOf revert for this projectId.
40
40
  vm.mockCallRevert(
41
- address(PROJECTS), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
41
+ address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
42
42
  );
43
43
 
44
44
  // After burn, owner() should return address(0) — NOT revert.
@@ -50,8 +50,9 @@ contract L65_BurnLockProtection is Test {
50
50
  /// Unauthorized error (not an unrecoverable ownerOf revert), making the contract
51
51
  /// behave as if ownership was renounced.
52
52
  function test_burnedProjectNFT_checkOwnerRevertsGracefully() public {
53
- uint256 projectId = PROJECTS.createFor(alice);
54
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
53
+ uint256 projectId = projects.createFor(alice);
54
+ // forge-lint: disable-next-line(unsafe-typecast)
55
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
55
56
 
56
57
  // Alice can call the protected method before burn.
57
58
  vm.prank(alice);
@@ -59,7 +60,7 @@ contract L65_BurnLockProtection is Test {
59
60
 
60
61
  // Simulate project NFT burn.
61
62
  vm.mockCallRevert(
62
- address(PROJECTS), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
63
+ address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
63
64
  );
64
65
 
65
66
  // After burn, nobody can call protected methods — but the revert is graceful
@@ -75,7 +76,7 @@ contract L65_BurnLockProtection is Test {
75
76
 
76
77
  /// @notice Address-based ownership is unaffected by the try-catch change.
77
78
  function test_addressBasedOwnership_unaffectedByTryCatch() public {
78
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
79
+ MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
79
80
 
80
81
  assertEq(ownable.owner(), alice, "Owner should be alice");
81
82
 
@@ -93,14 +94,15 @@ contract L65_BurnLockProtection is Test {
93
94
 
94
95
  /// @notice Normal project-based ownership still works correctly after the fix.
95
96
  function test_normalProjectOwnership_stillWorks() public {
96
- uint256 projectId = PROJECTS.createFor(alice);
97
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
97
+ uint256 projectId = projects.createFor(alice);
98
+ // forge-lint: disable-next-line(unsafe-typecast)
99
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
98
100
 
99
101
  assertEq(ownable.owner(), alice);
100
102
 
101
103
  // Transfer project NFT.
102
104
  vm.prank(alice);
103
- PROJECTS.transferFrom(alice, bob, projectId);
105
+ projects.transferFrom(alice, bob, projectId);
104
106
 
105
107
  assertEq(ownable.owner(), bob, "Owner should follow project NFT transfer");
106
108
 
@@ -10,19 +10,19 @@ import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
10
10
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
11
11
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
12
12
 
13
- /// @title L66_ZeroAddressValidation
14
- /// @notice Verifies that deploying with a zero-address PROJECTS
13
+ /// @title ZeroAddressValidation
14
+ /// @notice Verifies that deploying with a zero-address projects
15
15
  /// contract and a non-zero projectId reverts at construction time, preventing
16
16
  /// permanently broken project-based ownership.
17
- contract L66_ZeroAddressValidation is Test {
18
- IJBProjects PROJECTS;
19
- IJBPermissions PERMISSIONS;
17
+ contract ZeroAddressValidation is Test {
18
+ IJBProjects projects;
19
+ IJBPermissions permissions;
20
20
 
21
21
  address alice = makeAddr("alice");
22
22
 
23
23
  function setUp() public {
24
- PERMISSIONS = new JBPermissions(address(0));
25
- PROJECTS = new JBProjects(address(123), address(0), address(0));
24
+ permissions = new JBPermissions(address(0));
25
+ projects = new JBProjects(address(123), address(0), address(0));
26
26
  }
27
27
 
28
28
  /// @notice Deploying with projects=address(0) and non-zero projectId must revert.
@@ -30,7 +30,7 @@ contract L66_ZeroAddressValidation is Test {
30
30
  vm.expectRevert(
31
31
  abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
32
32
  );
33
- new MockOwnable(IJBProjects(address(0)), PERMISSIONS, address(0), uint88(1));
33
+ new MockOwnable(IJBProjects(address(0)), permissions, address(0), uint88(1));
34
34
  }
35
35
 
36
36
  /// @notice Fuzz: any non-zero projectId with projects=address(0) must revert.
@@ -40,27 +40,28 @@ contract L66_ZeroAddressValidation is Test {
40
40
  vm.expectRevert(
41
41
  abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
42
42
  );
43
- new MockOwnable(IJBProjects(address(0)), PERMISSIONS, address(0), projectId);
43
+ new MockOwnable(IJBProjects(address(0)), permissions, address(0), projectId);
44
44
  }
45
45
 
46
46
  /// @notice Deploying with projects=address(0) and projectId=0 (address-based ownership)
47
47
  /// should NOT revert for this error — it's valid as long as initialOwner != address(0).
48
48
  function test_zeroProjectsWithAddressOwnership_succeeds() public {
49
49
  // This is valid: address-based ownership with projects=address(0).
50
- MockOwnable ownable = new MockOwnable(IJBProjects(address(0)), PERMISSIONS, alice, uint88(0));
50
+ MockOwnable ownable = new MockOwnable(IJBProjects(address(0)), permissions, alice, uint88(0));
51
51
  assertEq(ownable.owner(), alice, "Owner should be alice with address-based ownership");
52
52
  }
53
53
 
54
- /// @notice Normal deployment with valid PROJECTS contract and projectId succeeds.
54
+ /// @notice Normal deployment with valid projects contract and projectId succeeds.
55
55
  function test_validProjectsWithProjectId_succeeds() public {
56
- uint256 projectId = PROJECTS.createFor(alice);
57
- MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
56
+ uint256 projectId = projects.createFor(alice);
57
+ // forge-lint: disable-next-line(unsafe-typecast)
58
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
58
59
  assertEq(ownable.owner(), alice, "Owner should be alice via project NFT");
59
60
  }
60
61
 
61
62
  /// @notice The existing check for both zero owner and zero projectId is still enforced.
62
63
  function test_bothZero_stillReverts() public {
63
64
  vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
64
- new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(0));
65
+ new MockOwnable(projects, permissions, address(0), uint88(0));
65
66
  }
66
67
  }