@bananapus/ownable-v6 0.0.33 → 0.0.36

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/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 instead of 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 project-scoped delegated permissions.
4
4
 
5
5
  ## Documentation
6
6
 
@@ -20,7 +20,8 @@ This package extends the standard ownership model in three ways:
20
20
 
21
21
  - ownership can point to a Juicebox project ID instead of an address
22
22
  - `owner()` can resolve dynamically to the current holder of that project NFT
23
- - delegated operators can satisfy `onlyOwner` through a configured `JBPermissions` permission ID
23
+ - project-owned contracts can let delegated operators satisfy `onlyOwner` through a configured `JBPermissions` permission ID
24
+ - address-owned contracts are direct-owner-only
24
25
 
25
26
  For contracts that are already meant to be owned by a project, this avoids manual ownership transfers when the project NFT changes hands.
26
27
 
@@ -41,7 +42,7 @@ If the issue is in project ownership itself, start in `nana-core-v6` and `JBProj
41
42
  This package is a small ownership adapter:
42
43
 
43
44
  1. resolve who the effective owner is
44
- 2. optionally allow a delegated permission to satisfy `onlyOwner`
45
+ 2. optionally allow a delegated permission to satisfy `onlyOwner` when the contract is project-owned
45
46
  3. preserve an `Ownable`-like interface for downstream contracts
46
47
 
47
48
  ## Read These Files First
@@ -54,7 +55,7 @@ This package is a small ownership adapter:
54
55
 
55
56
  - ownership may resolve to a project NFT holder instead of a fixed address, so caching `owner()` off-chain can go stale
56
57
  - `owner()` can resolve to `address(0)` if the referenced project NFT is invalid or unreadable, which effectively renounces the contract
57
- - delegated operator access depends on a chosen permission ID, not on a generic admin role
58
+ - delegated operator access only applies in project-owned mode and depends on a chosen permission ID, not on a generic admin role
58
59
  - explicit ownership transfers reset the permission ID, but project NFT transfers do not mutate stored owner data
59
60
  - a project NFT round trip back to the owner who last set `permissionId` can reactivate that owner's still-granted delegates
60
61
  - ownership transfer and permission-ID updates are part of the security model, not just convenience helpers
@@ -72,7 +73,8 @@ This package is a small ownership adapter:
72
73
  3. `test/RegressionUnmintedProjectHijack.t.sol`
73
74
  4. `test/regression/BurnLockProtection.t.sol`
74
75
  5. `test/regression/PermissionIdNFTTransfer.t.sol`
75
- 6. `test/audit/CodexNemesisPermissionReactivation.t.sol`
76
+ 6. `test/regression/StaleDelegateReactivationOnProjectReturn.t.sol`
77
+ 7. `test/regression/AddressOwnerPermissionPolicy.t.sol`
76
78
 
77
79
  ## Install
78
80
 
@@ -103,7 +105,8 @@ test/
103
105
  ## Risks And Notes
104
106
 
105
107
  - if ownership is tied to a project NFT and that NFT becomes unreachable, the contract is effectively locked
106
- - delegated access depends on a chosen permission ID, so bad permission selection is an operational risk
108
+ - project-owned delegated access depends on a chosen permission ID, so bad permission selection is an operational risk
109
+ - address-owned contracts cannot enable delegated owner access
107
110
  - permission IDs reset on explicit ownership transfers; project NFT transfers leave the ID stored but stale unless the
108
111
  resolved owner still matches the owner who set it
109
112
  - 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)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/ownable-v6",
3
- "version": "0.0.33",
3
+ "version": "0.0.36",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,8 @@
17
17
  "node": ">=20.0.0"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/core-v6": "^0.0.72",
21
- "@bananapus/permission-ids-v6": "^0.0.27",
20
+ "@bananapus/core-v6": "^0.0.78",
21
+ "@bananapus/permission-ids-v6": "^0.0.28",
22
22
  "@openzeppelin/contracts": "5.6.1"
23
23
  },
24
24
  "scripts": {
package/src/JBOwnable.sol CHANGED
@@ -67,12 +67,8 @@ contract JBOwnable is JBOwnableOverrides {
67
67
  {
68
68
  address resolvedNewOwner = newOwner;
69
69
  if (newProjectId != 0) {
70
- try PROJECTS.ownerOf(newProjectId) returns (address projectOwner) {
71
- resolvedNewOwner = projectOwner;
72
- } catch {
73
- // Pre-bound future projects have no visible owner yet, so the event reports address(0).
74
- resolvedNewOwner = address(0);
75
- }
70
+ // Pre-bound future projects have no visible owner yet, so the event reports address(0).
71
+ resolvedNewOwner = _projectOwnerOf(newProjectId);
76
72
  }
77
73
 
78
74
  emit OwnershipTransferred({previousOwner: previousOwner, newOwner: resolvedNewOwner, caller: _msgSender()});
@@ -12,7 +12,8 @@ import {JBOwner} from "./structs/JBOwner.sol";
12
12
 
13
13
  /// @notice Abstract base implementing Juicebox-aware ownership resolution, transfer, and permission delegation.
14
14
  /// Ownership is either address-based (a fixed EOA/contract) or project-based (whoever holds the project's ERC-721
15
- /// NFT). The owner can delegate access to other addresses by configuring a `permissionId` in `JBPermissions`.
15
+ /// NFT). A project-based owner can delegate access by configuring a `permissionId` in `JBPermissions`; address-based
16
+ /// ownership is direct-owner-only.
16
17
  /// @dev Project NFT transfers do not update stored owner data. A nonzero `permissionId` is only effective while the
17
18
  /// resolved owner still equals `_permissionOwner`, the owner who last set that ID. If the NFT leaves and later returns
18
19
  /// to that owner, their still-granted delegate permissions become effective again.
@@ -21,6 +22,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
21
22
  // --------------------------- custom errors ------------------------- //
22
23
  //*********************************************************************//
23
24
 
25
+ /// @notice Thrown when address-based ownership tries to enable `JBPermissions` delegation.
26
+ /// @param owner The address-based owner.
27
+ /// @param permissionId The nonzero permission ID being set.
28
+ error JBOwnableOverrides_AddressOwnerCannotSetPermissionId(address owner, uint8 permissionId);
29
+
24
30
  /// @notice Thrown when an ownership transfer or constructor input does not identify exactly one valid owner.
25
31
  /// @param newOwner The address owner being set.
26
32
  /// @param projectId The project owner ID being set.
@@ -61,7 +67,9 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
61
67
  /// the zero address as the `initialOwner`.
62
68
  /// To restrict access to a specific address, pass that address as the `initialOwner` and `0` as the
63
69
  /// `initialProjectIdOwner`.
64
- /// @dev The owner can give owner access to other addresses through the `permissions` contract.
70
+ /// @dev Project-based owners can give owner access to other addresses through the `permissions` contract.
71
+ /// Address-based owners cannot enable delegated owner access because `JBPermissions` project ID `0` is the
72
+ /// wildcard project namespace.
65
73
  /// @dev If `initialProjectIdOwner` references an unminted project, `owner()` resolves to `address(0)` and
66
74
  /// owner-gated calls revert until that project is created. The first account to mint that project becomes the
67
75
  /// effective owner, so deployers must control the mint sequence.
@@ -107,12 +115,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
107
115
  return ownerInfo.owner;
108
116
  }
109
117
 
110
- // If the project owner cannot be read, expose the owner as zero instead of bubbling the upstream revert.
111
- try PROJECTS.ownerOf(ownerInfo.projectId) returns (address projectOwner) {
112
- return projectOwner;
113
- } catch {
114
- return address(0);
115
- }
118
+ // Expose the project owner, or zero if the project's NFT cannot be read, instead of bubbling the revert.
119
+ return _projectOwnerOf(ownerInfo.projectId);
116
120
  }
117
121
 
118
122
  //*********************************************************************//
@@ -131,13 +135,18 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
131
135
  address resolvedOwner;
132
136
  if (ownerInfo.projectId == 0) {
133
137
  resolvedOwner = ownerInfo.owner;
138
+
139
+ // Address-owned contracts do not have a safe project namespace in JBPermissions: project ID 0 is the
140
+ // wildcard scope. Keep address-based ownership direct-owner-only.
141
+ if (_msgSender() != resolvedOwner) {
142
+ revert JBPermissioned.JBPermissioned_Unauthorized({
143
+ account: resolvedOwner, sender: _msgSender(), projectId: 0, permissionId: 0
144
+ });
145
+ }
146
+ return;
134
147
  } else {
135
148
  // Resolve the project owner dynamically; unreadable projects fail closed to address(0).
136
- try PROJECTS.ownerOf(ownerInfo.projectId) returns (address projectOwner) {
137
- resolvedOwner = projectOwner;
138
- } catch {
139
- resolvedOwner = address(0);
140
- }
149
+ resolvedOwner = _projectOwnerOf(ownerInfo.projectId);
141
150
  }
142
151
 
143
152
  // Ignore the stored permission ID while the project NFT is held by a different owner than the one who set it.
@@ -162,6 +171,22 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
162
171
  });
163
172
  }
164
173
 
174
+ /// @notice Resolves the current holder of a project's ownership NFT, or `address(0)` if the project's NFT cannot
175
+ /// be read.
176
+ /// @dev Wraps `PROJECTS.ownerOf` in a try-catch so an unreadable project (for example an NFT that has not been
177
+ /// minted yet) resolves to `address(0)` and owner-gated logic fails closed instead of bubbling the revert. The
178
+ /// resolution lives in one place so the try-catch lives in a single function rather than at every call site and in
179
+ /// every contract that inherits this.
180
+ /// @param projectId The ID of the project whose owner to resolve.
181
+ /// @return projectOwner The project's current owner, or `address(0)` if `PROJECTS.ownerOf` reverts.
182
+ function _projectOwnerOf(uint256 projectId) internal view virtual returns (address projectOwner) {
183
+ try PROJECTS.ownerOf(projectId) returns (address resolved) {
184
+ projectOwner = resolved;
185
+ } catch {
186
+ projectOwner = address(0);
187
+ }
188
+ }
189
+
165
190
  //*********************************************************************//
166
191
  // ---------------------- public transactions ------------------------ //
167
192
  //*********************************************************************//
@@ -173,10 +198,10 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
173
198
  _transferOwnership({newOwner: address(0), projectId: 0});
174
199
  }
175
200
 
176
- /// @notice Configures which `JBPermissions` permission ID grants delegate access to `onlyOwner` functions.
177
- /// Set to 0 to disable delegation entirely (only the direct owner can call).
178
- /// @dev Can only be called by the current owner. Records the current owner so delegation is ignored while a
179
- /// different owner holds the project NFT.
201
+ /// @notice Configures which `JBPermissions` permission ID grants delegate access to `onlyOwner` functions while
202
+ /// this contract is project-owned. Set to 0 to disable delegation entirely.
203
+ /// @dev Can only be called by the current owner. Address-owned contracts can only set `permissionId` to 0.
204
+ /// Records the current owner so delegation is ignored while a different owner holds the project NFT.
180
205
  /// @param permissionId The permission ID to use for `onlyOwner` delegation.
181
206
  function setPermissionId(uint8 permissionId) public virtual override {
182
207
  _checkOwner();
@@ -231,10 +256,16 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
231
256
  /// @param newProjectId The ID of the new owning project (zero if transferring to an address).
232
257
  function _emitTransferEvent(address previousOwner, address newOwner, uint88 newProjectId) internal virtual;
233
258
 
234
- /// @notice Sets the permission ID the current owner can use to delegate owner access.
235
- /// @dev Internal function without access restriction.
259
+ /// @notice Sets the permission ID the current project owner can use to delegate owner access.
260
+ /// @dev Internal function without access restriction. Address-owned contracts can only clear the permission ID.
236
261
  /// @param permissionId The permission ID to use for `onlyOwner`.
237
262
  function _setPermissionId(uint8 permissionId) internal virtual {
263
+ if (jbOwner.projectId == 0 && permissionId != 0) {
264
+ revert JBOwnableOverrides_AddressOwnerCannotSetPermissionId({
265
+ owner: jbOwner.owner, permissionId: permissionId
266
+ });
267
+ }
268
+
238
269
  jbOwner.permissionId = permissionId;
239
270
  _permissionOwner = owner();
240
271
  emit PermissionIdChanged({newId: permissionId, caller: _msgSender()});
@@ -265,11 +296,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
265
296
  if (ownerInfo.projectId == 0) {
266
297
  oldOwner = ownerInfo.owner;
267
298
  } else {
268
- try PROJECTS.ownerOf(ownerInfo.projectId) returns (address projectOwner) {
269
- oldOwner = projectOwner;
270
- } catch {
271
- oldOwner = address(0);
272
- }
299
+ oldOwner = _projectOwnerOf(ownerInfo.projectId);
273
300
  }
274
301
  // Explicit ownership transfers clear delegated access and the owner who authorized it.
275
302
  jbOwner = JBOwner({owner: newOwner, projectId: projectId, permissionId: 0});
@@ -4,8 +4,8 @@ pragma solidity ^0.8.0;
4
4
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
5
5
 
6
6
  /// @notice Interface for Juicebox-aware ownership. Supports two modes: address-based (a fixed EOA/contract owns the
7
- /// contract) or project-based (whoever holds a specific Juicebox project's ERC-721 NFT is the owner). The owner can
8
- /// delegate access to other addresses via `JBPermissions`.
7
+ /// contract) or project-based (whoever holds a specific Juicebox project's ERC-721 NFT is the owner). Project-based
8
+ /// owners can delegate access to other addresses via `JBPermissions`; address-based ownership is direct-owner-only.
9
9
  interface IJBOwnable {
10
10
  /// @notice Emitted when ownership is transferred to a new owner.
11
11
  /// @param previousOwner The address of the previous owner.
@@ -36,7 +36,8 @@ interface IJBOwnable {
36
36
  /// @notice Gives up ownership, making it impossible to call `onlyOwner` functions.
37
37
  function renounceOwnership() external;
38
38
 
39
- /// @notice Sets the permission ID the owner can use to give other addresses owner access.
39
+ /// @notice Sets the permission ID a project-based owner can use to give other addresses owner access.
40
+ /// @dev Address-based owners can only set `permissionId` to 0.
40
41
  /// @param permissionId The permission ID to use for `onlyOwner`.
41
42
  function setPermissionId(uint8 permissionId) external;
42
43