@bananapus/ownable-v6 0.0.19 → 0.0.20

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/ADMINISTRATION.md CHANGED
@@ -11,14 +11,14 @@
11
11
 
12
12
  ## Purpose
13
13
 
14
- `nana-ownable-v6` does not introduce a new admin surface by itself. It defines how ownership is resolved for other repos. The important control question is how a contract's `owner()` is determined and how delegated permission IDs behave across ownership transfers.
14
+ `nana-ownable-v6` does not add a new admin surface by itself. It defines how ownership is resolved for other repos. The important question is how a contract's `owner()` is determined and how delegated permission IDs behave across ownership transfers.
15
15
 
16
16
  ## Control Model
17
17
 
18
- - Ownership can be address-based or project-based.
19
- - Delegated operator checks run through `JBPermissions`.
20
- - Transfer and renounce semantics are part of the primitive.
21
- - Permission delegation resets on ownership transfer.
18
+ - ownership can be address-based or project-based
19
+ - delegated operator checks run through `JBPermissions`
20
+ - transfer and renounce semantics are part of the primitive
21
+ - delegated permission resets on ownership transfer
22
22
 
23
23
  ## Roles
24
24
 
@@ -40,34 +40,34 @@ The meaningful control surfaces are inherited by downstream contracts:
40
40
 
41
41
  ## Immutable And One-Way
42
42
 
43
- - Project ownership changes dynamically with project NFT transfers.
44
- - Delegated permission ID resets on ownership transfer.
45
- - Renouncing ownership is final unless the inheriting contract adds a separate recovery path.
43
+ - project ownership changes dynamically with project NFT transfers
44
+ - delegated permission ID resets on ownership transfer
45
+ - renouncing ownership is final unless the inheriting contract adds a separate recovery path
46
46
 
47
47
  ## Operational Notes
48
48
 
49
- - Treat project-based ownership as live routing, not a snapshot.
50
- - Do not assume an operator permission survives ownership transfer.
51
- - Treat `setPermissionId(...)` as a real authority change because it rewires which delegated permission bit counts as owner access.
52
- - Review the inheriting contract, not just this primitive, to understand the full admin surface.
49
+ - treat project-based ownership as live routing, not a snapshot
50
+ - do not assume an operator permission survives ownership transfer
51
+ - treat `setPermissionId(...)` as a real authority change because it rewires which delegated permission bit counts as owner access
52
+ - review the inheriting contract, not just this primitive, to understand the full admin surface
53
53
 
54
54
  ## Machine Notes
55
55
 
56
- - Do not conclude authority from this repo alone; follow the inheriting contract's `onlyOwner` surfaces.
57
- - Treat ownership transfer as potentially changing both the owner identity and the usable delegated permission ID.
58
- - If the current permission ID is undocumented, inspect `jbOwner.permissionId` before reasoning about delegated owner access.
59
- - If a downstream repo uses project-based ownership, re-evaluate owner resolution after every project NFT transfer.
56
+ - do not conclude authority from this repo alone; follow the inheriting contract's `onlyOwner` surfaces
57
+ - treat ownership transfer as potentially changing both the owner identity and the usable delegated permission ID
58
+ - if the current permission ID is undocumented, inspect `jbOwner.permissionId` before reasoning about delegated owner access
59
+ - if a downstream repo uses project-based ownership, re-evaluate owner resolution after every project NFT transfer
60
60
 
61
61
  ## Recovery
62
62
 
63
- - This primitive has no protocol-wide recovery surface.
64
- - If ownership was transferred to the wrong project or address, recovery depends on the inheriting contract still recognizing the current owner.
63
+ - this primitive has no protocol-wide recovery surface
64
+ - if ownership was transferred to the wrong project or address, recovery depends on the inheriting contract still recognizing the current owner
65
65
 
66
66
  ## Admin Boundaries
67
67
 
68
- - This repo does not create a new permission namespace.
69
- - It cannot make an inheriting contract safer than that contract's own privileged functions.
70
- - It cannot preserve delegated operators across ownership transfer by default.
68
+ - this repo does not create a new permission namespace
69
+ - it cannot make an inheriting contract safer than that contract's own privileged functions
70
+ - it cannot preserve delegated operators across ownership transfer by default
71
71
 
72
72
  ## Source Map
73
73
 
package/ARCHITECTURE.md CHANGED
@@ -6,15 +6,17 @@
6
6
 
7
7
  ## System Overview
8
8
 
9
- The repo is an ownership primitive, not a policy layer. `JBOwnable` exposes a familiar inheritance surface. `JBOwnableOverrides` implements dynamic owner resolution, ownership transfer, renounce behavior, and delegated permission checks. Ownership can follow the current holder of a Juicebox project NFT instead of being fixed to an address.
9
+ This repo is an ownership primitive, not a policy layer. `JBOwnable` gives downstream repos a familiar inheritance surface. `JBOwnableOverrides` implements dynamic owner resolution, ownership transfer, renounce behavior, and delegated permission checks.
10
+
11
+ Ownership can follow the current holder of a Juicebox project NFT instead of staying fixed to one address.
10
12
 
11
13
  ## Core Invariants
12
14
 
13
- - Project-owned contracts must resolve the owner dynamically from the current project NFT holder.
14
- - The delegated permission ID resets on ownership transfer.
15
- - Pointing ownership at an unminted project can temporarily lock the contract until that project exists.
16
- - A burned or otherwise unresolvable project NFT effectively renounces ownership.
17
- - This repo should stay a drop-in primitive, not grow product-specific access rules.
15
+ - project-owned contracts must resolve the owner dynamically from the current project NFT holder
16
+ - the delegated permission ID resets on ownership transfer
17
+ - pointing ownership at an unminted project can temporarily lock the contract until that project exists
18
+ - an invalid or otherwise unresolvable project NFT effectively renounces ownership
19
+ - this repo should stay a drop-in primitive, not grow product-specific access rules
18
20
 
19
21
  ## Modules
20
22
 
@@ -27,9 +29,9 @@ The repo is an ownership primitive, not a policy layer. `JBOwnable` exposes a fa
27
29
 
28
30
  ## Trust Boundaries
29
31
 
30
- - Ownership resolution depends on `JBProjects` and `JBPermissions` from `nana-core-v6`.
31
- - This repo does not create a new permission namespace.
32
- - Contracts that inherit from it may still add policy on top, but the resolution semantics here are infrastructure-level.
32
+ - ownership resolution depends on `JBProjects` and `JBPermissions` from `nana-core-v6`
33
+ - this repo does not create a new permission namespace
34
+ - inheriting contracts may add policy on top, but the resolution semantics here are infrastructure-level
33
35
 
34
36
  ## Critical Flows
35
37
 
@@ -45,20 +47,20 @@ onlyOwner modifier
45
47
 
46
48
  ## Accounting Model
47
49
 
48
- No treasury accounting lives here. The critical state is ownership resolution data and delegated permission ID.
50
+ No treasury accounting lives here. The important state is ownership resolution data and delegated permission ID.
49
51
 
50
52
  ## Security Model
51
53
 
52
- - Ownership resolution edge cases are more important than surface API shape.
53
- - Permission delegation is simple conceptually but security-sensitive because it composes with a global permission registry.
54
- - Unresolvable project ownership is intentionally fail-closed. If `PROJECTS.ownerOf()` cannot resolve, `onlyOwner` should stop working rather than inventing fallback authority.
54
+ - ownership resolution edge cases matter more than surface API shape
55
+ - permission delegation is simple but security-sensitive because it composes with a global permission registry
56
+ - unresolvable project ownership is intentionally fail-closed
55
57
 
56
58
  ## Safe Change Guide
57
59
 
58
- - Be conservative with transfer and renounce semantics.
59
- - If event emission or transfer behavior changes, inspect deployer wrappers and inheriting repos.
60
- - If project-based ownership semantics change, re-check unminted-project and unresolvable-project behavior explicitly.
61
- - Do not make delegated permission IDs sticky across ownership transfers.
60
+ - be conservative with transfer and renounce semantics
61
+ - if event emission or transfer behavior changes, inspect deployer wrappers and inheriting repos
62
+ - if project-based ownership semantics change, re-check unminted-project and unresolvable-project behavior explicitly
63
+ - do not make delegated permission IDs sticky across ownership transfers
62
64
 
63
65
  ## Canonical Checks
64
66
 
@@ -1,10 +1,11 @@
1
1
  # Audit Instructions
2
2
 
3
- This repo provides ownership helpers that can follow Juicebox project NFTs instead of a fixed EOA. It is a small repo with disproportionate privilege impact.
3
+ This repo provides ownership helpers that can follow Juicebox project NFTs instead of a fixed EOA. It is a small repo with outsized privilege impact.
4
4
 
5
5
  ## Audit Objective
6
6
 
7
7
  Find issues that:
8
+
8
9
  - let unauthorized actors satisfy owner checks
9
10
  - break ownership updates when a project NFT moves, burns, or locks
10
11
  - let override logic produce a different owner than the project system intends
@@ -13,6 +14,7 @@ Find issues that:
13
14
  ## Scope
14
15
 
15
16
  In scope:
17
+
16
18
  - `src/JBOwnable.sol`
17
19
  - `src/JBOwnableOverrides.sol`
18
20
  - `src/interfaces/`
@@ -25,7 +27,8 @@ In scope:
25
27
 
26
28
  ## Security Model
27
29
 
28
- These contracts abstract owner as a project-based identity. Downstream repos use them to:
30
+ These contracts abstract "owner" as a project-based identity. Downstream repos use them to:
31
+
29
32
  - treat a Juicebox project owner as contract owner
30
33
  - apply per-project override rules
31
34
  - keep admin power aligned with project NFT ownership instead of a static address
@@ -45,14 +48,12 @@ These contracts abstract “owner” as a project-based identity. Downstream rep
45
48
 
46
49
  ## Critical Invariants
47
50
 
48
- 1. Owner resolution is correct
49
- For any supported mode, `owner()` or equivalent checks must resolve to the intended authority and no one else.
50
-
51
- 2. Burn and lock behavior is safe
52
- If project ownership is intentionally burned or locked, the helper must not accidentally reopen control or brick valid admin paths.
53
-
54
- 3. Override precedence is coherent
55
- Overrides must not silently supersede project ownership in cases the design does not permit.
51
+ 1. Owner resolution is correct.
52
+ For any supported mode, `owner()` and owner checks must resolve to the intended authority and no one else.
53
+ 2. Burn and lock behavior is safe.
54
+ If project ownership is intentionally burned or locked, the helper must not accidentally reopen control or brick valid admin paths.
55
+ 3. Override precedence is coherent.
56
+ Overrides must not silently supersede project ownership in cases the design does not permit.
56
57
 
57
58
  ## Attack Surfaces
58
59
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Juicebox Ownable
2
2
 
3
- `@bananapus/ownable-v6` is an ownership helper for contracts that should be controlled by a Juicebox project rather than a fixed wallet. It keeps the familiar `Ownable` shape while letting ownership follow a project NFT and optional delegated permissions.
3
+ `@bananapus/ownable-v6` is an ownership helper for contracts that should be controlled by a Juicebox project instead of a fixed wallet. It keeps the familiar `Ownable` shape while letting ownership follow a project NFT and optional delegated permissions.
4
4
 
5
5
  Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
6
  User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
@@ -11,32 +11,32 @@ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
11
11
 
12
12
  ## Overview
13
13
 
14
- This package extends the standard ownership model in three useful ways:
14
+ This package extends the standard ownership model in three ways:
15
15
 
16
16
  - ownership can point to a Juicebox project ID instead of an address
17
- - `owner()` resolves dynamically to the current holder of that project NFT when the referenced project remains readable
18
- - delegated operators can satisfy `onlyOwner` through a configurable `JBPermissions` permission ID
17
+ - `owner()` can resolve dynamically to the current holder of that project NFT
18
+ - delegated operators can satisfy `onlyOwner` through a configured `JBPermissions` permission ID
19
19
 
20
- For contracts that are already conceptually "owned by the project," this avoids manual ownership transfers when the project NFT changes hands.
20
+ For contracts that are already meant to be owned by a project, this avoids manual ownership transfers when the project NFT changes hands.
21
21
 
22
- Use this repo when ownership should follow a Juicebox project. Do not use it if plain single-address ownership is good enough; standard `Ownable` is simpler.
22
+ Use this repo when ownership should follow a Juicebox project. Do not use it if plain single-address ownership is enough. Standard `Ownable` is simpler.
23
23
 
24
- If your issue is in project ownership itself, start in `nana-core-v6` and `JBProjects`. This repo starts mattering when another contract wants its own admin surface to follow that project ownership.
24
+ If the issue is in project ownership itself, start in `nana-core-v6` and `JBProjects`. This repo matters when another contract wants its admin surface to follow that project ownership.
25
25
 
26
26
  ## Key Contracts
27
27
 
28
28
  | Contract | Role |
29
29
  | --- | --- |
30
- | `JBOwnable` | Concrete contract to inherit when you want Juicebox-aware ownership with the standard `onlyOwner` interface. |
31
- | `JBOwnableOverrides` | Abstract base that holds the owner-resolution and permission-checking logic. |
30
+ | `JBOwnable` | Concrete contract to inherit when you want Juicebox-aware ownership with a standard `onlyOwner` interface. |
31
+ | `JBOwnableOverrides` | Abstract base that holds owner resolution and delegated-permission logic. |
32
32
  | `IJBOwnable` | Interface for queries, transfers, permission ID changes, and events. |
33
33
 
34
34
  ## Mental Model
35
35
 
36
- This package is a thin ownership adapter:
36
+ This package is a small ownership adapter:
37
37
 
38
38
  1. resolve who the effective owner is
39
- 2. optionally delegate `onlyOwner` through a permission ID
39
+ 2. optionally allow a delegated permission to satisfy `onlyOwner`
40
40
  3. preserve an `Ownable`-like interface for downstream contracts
41
41
 
42
42
  ## Read These Files First
@@ -47,16 +47,16 @@ This package is a thin ownership adapter:
47
47
 
48
48
  ## Integration Traps
49
49
 
50
- - ownership may resolve to a project NFT holder rather than a fixed address, so caching `owner()` off-chain can become stale
51
- - `owner()` can resolve to `address(0)` if the referenced project NFT is burned, invalid, or otherwise unreadable, which effectively renounces the contract
50
+ - ownership may resolve to a project NFT holder instead of a fixed address, so caching `owner()` off-chain can go stale
51
+ - `owner()` can resolve to `address(0)` if the referenced project NFT is invalid or unreadable, which effectively renounces the contract
52
52
  - delegated operator access depends on a chosen permission ID, not on a generic admin role
53
53
  - ownership transfer and permission-ID updates are part of the security model, not just convenience helpers
54
54
 
55
55
  ## Where State Lives
56
56
 
57
- - effective ownership configuration lives in `JBOwnableOverrides`
58
- - downstream contract state still lives in the inheriting contract, not in this package
59
- - project ownership truth lives in `nana-core-v6` when the owner target is a Juicebox project
57
+ - effective ownership configuration: `JBOwnableOverrides`
58
+ - downstream contract state: the inheriting contract
59
+ - project ownership truth: `nana-core-v6` when the owner target is a Juicebox project
60
60
 
61
61
  ## High-Signal Tests
62
62
 
@@ -94,9 +94,9 @@ test/
94
94
  ## Risks And Notes
95
95
 
96
96
  - if ownership is tied to a project NFT and that NFT becomes unreachable, the contract is effectively locked
97
- - delegated access depends on a chosen permission ID, so collisions with other permission schemes are an operational risk
98
- - permission IDs reset on ownership transfer, which is safer by default but easy to miss if an integration expects long-lived operator access
99
- - transferring ownership to a project validates that the project exists, but later project-NFT invalidation can still collapse effective ownership to `address(0)`
97
+ - delegated access depends on a chosen permission ID, so bad permission selection is an operational risk
98
+ - permission IDs reset on ownership transfer, which is safer by default but easy to miss
99
+ - transferring ownership to a project validates that the project exists at transfer time, but later project invalidation can still collapse effective ownership to `address(0)`
100
100
 
101
101
  ## For AI Agents
102
102
 
package/SKILLS.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## Use This File For
4
4
 
5
- - Use this file when the task involves project-based ownership, delegated `onlyOwner` permissions, or how ownership should follow a Juicebox project NFT instead of a fixed wallet.
6
- - Start here, then decide whether the question is about owner resolution, permission delegation, or ownership transfer semantics. Those surfaces are intentionally compact but security-sensitive.
5
+ - Use this file when the task involves project-based ownership, delegated `onlyOwner` permissions, or ownership that should follow a Juicebox project NFT instead of a fixed wallet.
6
+ - Start here, then decide whether the question is about owner resolution, permission delegation, or ownership transfer semantics.
7
7
 
8
8
  ## Read This Next
9
9
 
@@ -25,7 +25,7 @@
25
25
 
26
26
  ## Purpose
27
27
 
28
- Ownership adapter for contracts that should follow Juicebox project ownership instead of a fixed address, with optional delegated permission IDs layered on top of the familiar `Ownable` pattern.
28
+ Ownership adapter for contracts that should follow Juicebox project ownership instead of a fixed address, with optional delegated permission IDs on top of the familiar `Ownable` pattern.
29
29
 
30
30
  ## Reference Files
31
31
 
@@ -36,6 +36,6 @@ Ownership adapter for contracts that should follow Juicebox project ownership in
36
36
 
37
37
  - Start in [`src/JBOwnableOverrides.sol`](./src/JBOwnableOverrides.sol) when the question is about who the effective owner is or why `onlyOwner` passed or failed.
38
38
  - Treat ownership transfer and delegated permission resets as security-sensitive.
39
- - Project-based ownership can intentionally become unusable if it points at an unminted or invalid project. Treat that as a deployment invariant, not a runtime surprise.
39
+ - Project-based ownership can intentionally become unusable if it points at an unminted or invalid project.
40
40
  - Unminted or unexpectedly transferred project NFTs can change the effective owner surface. Check the project lifecycle, not just this adapter.
41
- - When a bug looks like project ownership itself, confirm whether the real source is upstream in `nana-core-v6` rather than this adapter layer.
41
+ - When a bug looks like project ownership itself, confirm whether the real source is upstream in `nana-core-v6` rather than this adapter.
package/USER_JOURNEYS.md CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  ## Repo Purpose
4
4
 
5
- This repo adapts `Ownable`-style control to Juicebox project ownership and project-scoped operator permissions.
6
- It is an ownership adapter. It does not replace the underlying ownership or permission registries in
7
- [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md).
5
+ This repo adapts `Ownable`-style control to Juicebox project ownership and project-scoped operator permissions. It is an ownership adapter. It does not replace the underlying ownership or permission registries in [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md).
8
6
 
9
7
  ## Primary Actors
10
8
 
@@ -31,7 +29,7 @@ It is an ownership adapter. It does not replace the underlying ownership or perm
31
29
  **Main Flow**
32
30
  1. Inherit `JBOwnable` or `JBOwnableOverrides`.
33
31
  2. Initialize ownership with the relevant project ID and `JBProjects` reference.
34
- 3. Let `owner()` resolve through the current project NFT holder rather than a fixed address.
32
+ 3. Let `owner()` resolve through the current project NFT holder instead of a fixed address.
35
33
 
36
34
  **Failure Modes**
37
35
  - the contract assumes ordinary `Ownable` transfer semantics after adopting project-based ownership
@@ -59,7 +57,7 @@ It is an ownership adapter. It does not replace the underlying ownership or perm
59
57
  **Failure Modes**
60
58
  - teams grant a broader permission than intended
61
59
  - downstream reviewers forget that `onlyOwner` may resolve through permissions instead of direct ownership
62
- - operators retain stale permissions after governance changes
60
+ - operators keep stale permissions after governance changes
63
61
 
64
62
  **Postconditions**
65
63
  - the chosen operator can satisfy `onlyOwner` without receiving direct ownership of the project or contract
@@ -99,11 +97,11 @@ It is an ownership adapter. It does not replace the underlying ownership or perm
99
97
  **Main Flow**
100
98
  1. Use `transferOwnership(...)` for an address owner or `transferOwnershipToProject(...)` for a project owner.
101
99
  2. Re-establish delegated permission policy if the new owner still wants operators.
102
- 3. Renounce or burn only when permanent admin loss is intentional.
100
+ 3. Renounce only when permanent admin loss is intentional.
103
101
 
104
102
  **Failure Modes**
105
103
  - ownership is burned even though the downstream contract still needs administration
106
- - teams forget that permission-ID delegation resets across ownership changes
104
+ - teams forget that delegated permissions reset across ownership changes
107
105
 
108
106
  **Postconditions**
109
107
  - control moves to the chosen address or project, or is intentionally removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/ownable-v6",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -134,6 +134,17 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
134
134
  }
135
135
  }
136
136
 
137
+ // When permissionId is 0 (direct-owner-only mode), bypass the permission system entirely.
138
+ // This ensures ROOT operators cannot act as owner when delegation is disabled.
139
+ if (ownerInfo.permissionId == 0) {
140
+ if (_msgSender() != resolvedOwner) {
141
+ revert JBPermissioned.JBPermissioned_Unauthorized({
142
+ account: resolvedOwner, sender: _msgSender(), projectId: ownerInfo.projectId, permissionId: 0
143
+ });
144
+ }
145
+ return;
146
+ }
147
+
137
148
  _requirePermissionFrom({
138
149
  account: resolvedOwner, projectId: ownerInfo.projectId, permissionId: ownerInfo.permissionId
139
150
  });
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+
6
+ import {MockOwnable} from "../mocks/MockOwnable.sol";
7
+
8
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
9
+ import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
10
+ import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
11
+ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
12
+
13
+ contract RootPermissionBypassesPermissionIdZeroTest is Test {
14
+ JBProjects internal projects;
15
+ JBPermissions internal permissions;
16
+
17
+ address internal alice = makeAddr("alice");
18
+ address internal operator = makeAddr("operator");
19
+
20
+ function setUp() public {
21
+ permissions = new JBPermissions(address(0));
22
+ projects = new JBProjects(address(this), address(0), address(0));
23
+ }
24
+
25
+ /// @notice After M-39 fix: ROOT operator is rejected when permissionId=0 (direct-owner-only mode).
26
+ function test_rootPermissionRejectedWhenPermissionIdIsZero() public {
27
+ uint256 projectId = projects.createFor(alice);
28
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
29
+
30
+ // Grant ROOT permission (id=1) to operator.
31
+ uint8[] memory permissionIds = new uint8[](1);
32
+ permissionIds[0] = 1;
33
+
34
+ vm.prank(alice);
35
+ permissions.setPermissionsFor(
36
+ alice, JBPermissionsData({operator: operator, projectId: uint56(projectId), permissionIds: permissionIds})
37
+ );
38
+
39
+ (, uint88 storedProjectId, uint8 permissionId) = ownable.jbOwner();
40
+ assertEq(storedProjectId, projectId);
41
+ assertEq(permissionId, 0, "expected direct-owner-only mode");
42
+
43
+ // Operator should be rejected when permissionId=0.
44
+ vm.prank(operator);
45
+ vm.expectRevert(
46
+ abi.encodeWithSelector(JBPermissioned.JBPermissioned_Unauthorized.selector, alice, operator, projectId, 0)
47
+ );
48
+ ownable.protectedMethod();
49
+ }
50
+
51
+ /// @notice Direct owner still works when permissionId=0.
52
+ function test_directOwnerStillWorksWithPermissionIdZero() public {
53
+ uint256 projectId = projects.createFor(alice);
54
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
55
+
56
+ (, uint88 storedProjectId, uint8 permissionId) = ownable.jbOwner();
57
+ assertEq(storedProjectId, projectId);
58
+ assertEq(permissionId, 0);
59
+
60
+ // Alice (project owner) should still be able to call the protected method.
61
+ vm.prank(alice);
62
+ ownable.protectedMethod();
63
+ }
64
+
65
+ /// @notice Non-zero permissionId still delegates correctly via the permission system.
66
+ function test_delegatedOperatorWorksWhenPermissionIdNonZero() public {
67
+ uint256 projectId = projects.createFor(alice);
68
+ MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
69
+
70
+ // Set permissionId to 42 (non-zero = delegation enabled).
71
+ vm.prank(alice);
72
+ ownable.setPermissionId(42);
73
+
74
+ // Grant permission 42 to operator.
75
+ uint8[] memory permissionIds = new uint8[](1);
76
+ permissionIds[0] = 42;
77
+
78
+ vm.prank(alice);
79
+ permissions.setPermissionsFor(
80
+ alice, JBPermissionsData({operator: operator, projectId: uint56(projectId), permissionIds: permissionIds})
81
+ );
82
+
83
+ // Operator should succeed with matching permissionId.
84
+ vm.prank(operator);
85
+ ownable.protectedMethod();
86
+ }
87
+ }