@bananapus/ownable-v6 0.0.23 → 0.0.25

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
@@ -62,7 +62,7 @@ This package is a small ownership adapter:
62
62
 
63
63
  1. `test/Ownable.t.sol`
64
64
  2. `test/OwnableAttacks.t.sol`
65
- 3. `test/CodexUnmintedProjectHijack.t.sol`
65
+ 3. `test/RegressionUnmintedProjectHijack.t.sol`
66
66
  4. `test/regression/BurnLockProtection.t.sol`
67
67
 
68
68
  ## Install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/ownable-v6",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,8 +19,8 @@
19
19
  "node": ">=20.0.0"
20
20
  },
21
21
  "dependencies": {
22
- "@bananapus/core-v6": "0.0.38",
23
- "@bananapus/permission-ids-v6": "0.0.22",
22
+ "@bananapus/core-v6": "^0.0.54",
23
+ "@bananapus/permission-ids-v6": "^0.0.25",
24
24
  "@openzeppelin/contracts": "5.6.1"
25
25
  },
26
26
  "scripts": {
@@ -16,4 +16,4 @@
16
16
  - [`test/Ownable.t.sol`](../test/Ownable.t.sol) for baseline behavior.
17
17
  - [`test/OwnableEdgeCases.t.sol`](../test/OwnableEdgeCases.t.sol) and [`test/OwnableAttacks.t.sol`](../test/OwnableAttacks.t.sol) for edge and adversarial cases.
18
18
  - [`test/OwnableInvariantTests.sol`](../test/OwnableInvariantTests.sol) for broader invariants.
19
- - [`test/regression/BurnLockProtection.t.sol`](../test/regression/BurnLockProtection.t.sol), [`test/regression/ZeroAddressValidation.t.sol`](../test/regression/ZeroAddressValidation.t.sol), and [`test/CodexUnmintedProjectHijack.t.sol`](../test/CodexUnmintedProjectHijack.t.sol) for the regressions most likely to matter in review.
19
+ - [`test/regression/BurnLockProtection.t.sol`](../test/regression/BurnLockProtection.t.sol), [`test/regression/ZeroAddressValidation.t.sol`](../test/regression/ZeroAddressValidation.t.sol), and [`test/RegressionUnmintedProjectHijack.t.sol`](../test/RegressionUnmintedProjectHijack.t.sol) for the regressions most likely to matter in review.
package/src/JBOwnable.sol CHANGED
@@ -7,16 +7,11 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
7
7
 
8
8
  import {JBOwnableOverrides} from "./JBOwnableOverrides.sol";
9
9
 
10
- /// @notice A function restricted by `JBOwnable` can only be called by a Juicebox project's owner, a specified owner
11
- /// address (if set), or addresses with permission from the owner.
12
- /// @dev A function with the `onlyOwner` modifier from `JBOwnable` can only be called by addresses with owner access
13
- /// based on a `JBOwner` struct:
14
- /// 1. If `JBOwner.projectId` isn't zero, the address holding the `JBProjects` NFT with the `JBOwner.projectId` ID is
15
- /// the owner.
16
- /// 2. If `JBOwner.projectId` is set to `0`, the `JBOwner.owner` address is the owner.
17
- /// 3. The owner can give other addresses access with `JBPermissions.setPermissionsFor(...)`, using the
18
- /// `JBOwner.permissionId` permission.
19
- /// @dev To use `onlyOwner`, inherit this contract and apply the modifier to a function.
10
+ /// @notice Juicebox-aware ownership for any contract. Inherit this and apply the `onlyOwner` modifier to restrict
11
+ /// functions to the project owner, a fixed address, or anyone the owner has granted permission to via `JBPermissions`.
12
+ /// @dev Ownership resolves dynamically: if `JBOwner.projectId` is set, the holder of that project's ERC-721 NFT is
13
+ /// the owner. If `projectId` is 0, the stored `JBOwner.owner` address is used instead. The owner can delegate access
14
+ /// to other addresses by setting a `permissionId` and granting that permission through `JBPermissions`.
20
15
  contract JBOwnable is JBOwnableOverrides {
21
16
  //*********************************************************************//
22
17
  // -------------------------- constructor ---------------------------- //
@@ -10,30 +10,43 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
10
10
  import {IJBOwnable} from "./interfaces/IJBOwnable.sol";
11
11
  import {JBOwner} from "./structs/JBOwner.sol";
12
12
 
13
- /// @notice An abstract base for `JBOwnable`, which restricts functions so they can only be called by a Juicebox
14
- /// project's owner or a specific owner address. The owner can give access permission to other addresses with
15
- /// `JBPermissions`.
13
+ /// @notice Abstract base implementing Juicebox-aware ownership resolution, transfer, and permission delegation.
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`.
16
+ /// @dev Stale permission detection: when ownership changes (e.g. project NFT transferred), the `permissionId` is
17
+ /// effectively ignored until the new owner explicitly re-sets it — preventing the previous owner's delegates from
18
+ /// retaining access.
16
19
  abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
17
20
  //*********************************************************************//
18
21
  // --------------------------- custom errors ------------------------- //
19
22
  //*********************************************************************//
20
23
 
21
- error JBOwnableOverrides_InvalidNewOwner();
22
- error JBOwnableOverrides_ProjectDoesNotExist();
23
- error JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner();
24
+ /// @notice Thrown when an ownership transfer or constructor input does not identify exactly one valid owner.
25
+ /// @param newOwner The address owner being set.
26
+ /// @param projectId The project owner ID being set.
27
+ error JBOwnableOverrides_InvalidNewOwner(address newOwner, uint256 projectId);
28
+
29
+ /// @notice Thrown when project-based ownership points to a project that has not been minted.
30
+ /// @param projectId The project ID that was requested.
31
+ /// @param projectCount The current number of minted projects.
32
+ error JBOwnableOverrides_ProjectDoesNotExist(uint256 projectId, uint256 projectCount);
33
+
34
+ /// @notice Thrown when project-based ownership is requested without a `JBProjects` contract.
35
+ /// @param projectId The project owner ID that requires a non-zero `JBProjects` contract.
36
+ error JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner(uint256 projectId);
24
37
 
25
38
  //*********************************************************************//
26
39
  // ---------------- public immutable stored properties --------------- //
27
40
  //*********************************************************************//
28
41
 
29
- /// @notice Mints ERC-721s that represent project ownership and transfers.
42
+ /// @notice The `JBProjects` ERC-721 contract used to resolve project-based ownership.
30
43
  IJBProjects public immutable override PROJECTS;
31
44
 
32
45
  //*********************************************************************//
33
46
  // --------------------- public stored properties -------------------- //
34
47
  //*********************************************************************//
35
48
 
36
- /// @notice This contract's owner information.
49
+ /// @notice The current ownership state — who owns this contract and how permission delegation is configured.
37
50
  JBOwner public override jbOwner;
38
51
 
39
52
  //*********************************************************************//
@@ -76,7 +89,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
76
89
  // Deploying with projects=address(0) and a non-zero projectId would permanently disable
77
90
  // ownership resolution, as all ownerOf() calls would revert on the zero address.
78
91
  if (initialProjectIdOwner != 0 && address(projects) == address(0)) {
79
- revert JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner();
92
+ revert JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner({projectId: initialProjectIdOwner});
80
93
  }
81
94
 
82
95
  // We force the inheriting contract to set an owner, as there is a low chance someone will use `JBOwnable` to
@@ -84,7 +97,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
84
97
  // It's more likely both were accidentally set to `0`. If you really want an unowned contract, set the owner to
85
98
  // an address and call `renounceOwnership()` in the constructor body.
86
99
  if (initialProjectIdOwner == 0 && initialOwner == address(0)) {
87
- revert JBOwnableOverrides_InvalidNewOwner();
100
+ revert JBOwnableOverrides_InvalidNewOwner({newOwner: initialOwner, projectId: initialProjectIdOwner});
88
101
  }
89
102
 
90
103
  // No explicit project existence check here — if `initialProjectIdOwner` refers to an unminted project,
@@ -98,12 +111,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
98
111
  // -------------------------- public views --------------------------- //
99
112
  //*********************************************************************//
100
113
 
101
- /// @notice Returns the owner's address based on this contract's `JBOwner`.
102
- /// @dev If `projectId` is non-zero, resolves via `PROJECTS.ownerOf()`. If that call reverts (e.g., because the
103
- /// project NFT was burned or invalidated), returns `address(0)` effectively treating the contract as renounced.
104
- /// @dev **Assumption:** `JBProjects` V6 has no burn function, so this scenario cannot occur under normal
105
- /// conditions. The try-catch is a defensive measure against hypothetical future changes to `JBProjects` or
106
- /// unexpected ERC-721 behavior.
114
+ /// @notice Returns the current owner's address. If ownership is project-based, this dynamically resolves to
115
+ /// whoever holds the project's ERC-721 NFT right now.
116
+ /// @dev If `projectId` is non-zero, resolves via `PROJECTS.ownerOf()`. If that call reverts (e.g., burned NFT),
117
+ /// returns `address(0)` — effectively treating the contract as renounced. `JBProjects` V6 has no burn function,
118
+ /// so this is a defensive measure only.
107
119
  function owner() public view virtual returns (address) {
108
120
  JBOwner memory ownerInfo = jbOwner;
109
121
 
@@ -124,9 +136,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
124
136
  // -------------------------- internal views ------------------------- //
125
137
  //*********************************************************************//
126
138
 
127
- /// @notice Reverts if the sender is not the owner.
139
+ /// @notice Reverts if the caller is not the owner (or an authorized delegate when `permissionId` is set).
128
140
  /// @dev If `projectId` is non-zero and `PROJECTS.ownerOf()` reverts (e.g., burned NFT), the resolved owner is
129
141
  /// `address(0)`, causing all `_checkOwner` calls to revert — equivalent to a renounced contract.
142
+ /// @dev Stale permission detection: if the resolved owner differs from `_permissionOwner` (set when
143
+ /// `setPermissionId` was last called), delegation is disabled until the new owner re-configures it.
130
144
  function _checkOwner() internal view virtual {
131
145
  JBOwner memory ownerInfo = jbOwner;
132
146
 
@@ -169,17 +183,18 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
169
183
  // ---------------------- public transactions ------------------------ //
170
184
  //*********************************************************************//
171
185
 
172
- /// @notice Gives up ownership of this contract, making it impossible to call `onlyOwner` and `_checkOwner`
173
- /// functions.
174
- /// @dev This can only be called by the current owner.
186
+ /// @notice Permanently gives up ownership. After this, no address can call `onlyOwner` functions.
187
+ /// @dev Can only be called by the current owner. This is irreversible.
175
188
  function renounceOwnership() public virtual override {
176
189
  _checkOwner();
177
190
  _transferOwnership({newOwner: address(0), projectId: 0});
178
191
  }
179
192
 
180
- /// @notice Sets the permission ID the owner can use to give other addresses owner access.
181
- /// @dev This can only be called by the current owner.
182
- /// @param permissionId The permission ID to use for `onlyOwner`.
193
+ /// @notice Configures which `JBPermissions` permission ID grants delegate access to `onlyOwner` functions.
194
+ /// Set to 0 to disable delegation entirely (only the direct owner can call).
195
+ /// @dev Can only be called by the current owner. Records the current owner so stale permissions are detected
196
+ /// if ownership later changes.
197
+ /// @param permissionId The permission ID to use for `onlyOwner` delegation.
183
198
  function setPermissionId(uint8 permissionId) public virtual override {
184
199
  _checkOwner();
185
200
  _setPermissionId(permissionId);
@@ -193,26 +208,27 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
193
208
  function transferOwnership(address newOwner) public virtual override {
194
209
  _checkOwner();
195
210
  if (newOwner == address(0)) {
196
- revert JBOwnableOverrides_InvalidNewOwner();
211
+ revert JBOwnableOverrides_InvalidNewOwner({newOwner: newOwner, projectId: 0});
197
212
  }
198
213
 
199
214
  _transferOwnership({newOwner: newOwner, projectId: 0});
200
215
  }
201
216
 
202
- /// @notice Transfer ownership of this contract to a new Juicebox project.
203
- /// @dev The `permissionId` is reset to 0 on transfer to prevent permission clashes for the new project owner.
204
- /// The new owner must explicitly call `setPermissionId()` to configure owner-level permission delegation.
205
- /// @dev The `projectId` must fit within a `uint88`.
217
+ /// @notice Transfers ownership to a Juicebox project whoever holds that project's ERC-721 NFT becomes the
218
+ /// owner.
219
+ /// @dev The `permissionId` is reset to 0 on transfer to prevent the previous owner's delegates from retaining
220
+ /// access. The new project owner must call `setPermissionId()` to re-enable delegation.
221
+ /// @dev The `projectId` must fit within a `uint88` and the project must already exist.
206
222
  /// @param projectId The ID of the project to transfer ownership to.
207
223
  function transferOwnershipToProject(uint256 projectId) public virtual override {
208
224
  _checkOwner();
209
225
  if (projectId == 0 || projectId > type(uint88).max) {
210
- revert JBOwnableOverrides_InvalidNewOwner();
226
+ revert JBOwnableOverrides_InvalidNewOwner({newOwner: address(0), projectId: projectId});
211
227
  }
212
228
 
213
229
  // Make sure the project exists to prevent permanent loss of contract control.
214
230
  if (projectId > PROJECTS.count()) {
215
- revert JBOwnableOverrides_ProjectDoesNotExist();
231
+ revert JBOwnableOverrides_ProjectDoesNotExist({projectId: projectId, projectCount: PROJECTS.count()});
216
232
  }
217
233
 
218
234
  // forge-lint: disable-next-line(unsafe-typecast)
@@ -240,7 +256,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
240
256
  emit PermissionIdChanged({newId: permissionId, caller: _msgSender()});
241
257
  }
242
258
 
243
- /// @notice Helper to allow for drop-in replacement of OpenZeppelin `Ownable`.
259
+ /// @notice Drop-in replacement for OpenZeppelin's `Ownable._transferOwnership(address)`.
244
260
  /// @param newOwner The address that should receive ownership of this contract.
245
261
  function _transferOwnership(address newOwner) internal virtual {
246
262
  _transferOwnership({newOwner: newOwner, projectId: 0});
@@ -255,7 +271,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
255
271
  function _transferOwnership(address newOwner, uint88 projectId) internal virtual {
256
272
  // Can't set both a new owner and a new project ID.
257
273
  if (projectId != 0 && newOwner != address(0)) {
258
- revert JBOwnableOverrides_InvalidNewOwner();
274
+ revert JBOwnableOverrides_InvalidNewOwner({newOwner: newOwner, projectId: projectId});
259
275
  }
260
276
  // Load the owner information from storage.
261
277
  JBOwner memory ownerInfo = jbOwner;
@@ -3,7 +3,9 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
5
5
 
6
- /// @notice Provides Juicebox-aware ownership with support for project-based and address-based owners.
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
9
  interface IJBOwnable {
8
10
  /// @notice Emitted when ownership is transferred to a new owner.
9
11
  /// @param previousOwner The address of the previous owner.
@@ -16,14 +18,14 @@ interface IJBOwnable {
16
18
  /// @param caller The address that changed the permission ID.
17
19
  event PermissionIdChanged(uint8 newId, address caller);
18
20
 
19
- /// @notice The contract that mints ERC-721s representing project ownership.
21
+ /// @notice The `JBProjects` ERC-721 contract used to resolve project-based ownership.
20
22
  /// @return projects The `IJBProjects` contract.
21
23
  function PROJECTS() external view returns (IJBProjects projects);
22
24
 
23
- /// @notice This contract's owner information.
25
+ /// @notice The current ownership state — who owns this contract and how permission delegation is configured.
24
26
  /// @return owner The owner address (used when `projectId` is 0).
25
- /// @return projectId The ID of the Juicebox project whose owner is this contract's owner (0 if not project-owned).
26
- /// @return permissionId The permission ID the owner can use to grant other addresses owner access.
27
+ /// @return projectId The Juicebox project whose NFT holder is the owner (0 if address-based ownership).
28
+ /// @return permissionId The permission ID that delegates can use to act as owner via `JBPermissions`.
27
29
  function jbOwner() external view returns (address owner, uint88 projectId, uint8 permissionId);
28
30
 
29
31
  /// @notice Returns the current owner's address.
@@ -1,12 +1,12 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- /// @notice Owner information for a given instance of `JBOwnableOverrides`.
5
- /// @custom:member owner If `projectId` is 0, this address has owner access.
6
- /// @custom:member projectId The owner of the `JBProjects` ERC-721 with this ID has owner access. If this is 0, the
7
- /// `owner` address has owner access.
8
- /// @custom:member permissionId The permission ID which corresponds to owner access. See `JBPermissions` in `nana-core`
9
- /// and `nana-permission-ids`.
4
+ /// @notice Describes who owns a `JBOwnable` contract and how they can delegate access.
5
+ /// @custom:member owner The owner address — only used when `projectId` is 0 (address-based ownership mode).
6
+ /// @custom:member projectId If non-zero, the holder of this Juicebox project's ERC-721 NFT is the owner. When set,
7
+ /// the `owner` field is ignored and ownership resolves dynamically via `JBProjects.ownerOf(projectId)`.
8
+ /// @custom:member permissionId The permission ID that delegates can hold (via `JBPermissions`) to act as owner. Set
9
+ /// to 0 to disable delegation entirely — only the direct owner can call `onlyOwner` functions.
10
10
  struct JBOwner {
11
11
  address owner;
12
12
  uint88 projectId;