@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 +1 -1
- package/package.json +3 -3
- package/references/runtime.md +1 -1
- package/src/JBOwnable.sol +5 -10
- package/src/JBOwnableOverrides.sol +48 -32
- package/src/interfaces/IJBOwnable.sol +7 -5
- package/src/structs/JBOwner.sol +6 -6
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/
|
|
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.
|
|
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.
|
|
23
|
-
"@bananapus/permission-ids-v6": "0.0.
|
|
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": {
|
package/references/runtime.md
CHANGED
|
@@ -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/
|
|
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
|
|
11
|
-
///
|
|
12
|
-
/// @dev
|
|
13
|
-
///
|
|
14
|
-
///
|
|
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
|
|
14
|
-
///
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
102
|
-
///
|
|
103
|
-
///
|
|
104
|
-
///
|
|
105
|
-
///
|
|
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
|
|
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
|
|
173
|
-
///
|
|
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
|
|
181
|
-
///
|
|
182
|
-
/// @
|
|
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
|
|
203
|
-
///
|
|
204
|
-
/// The
|
|
205
|
-
///
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
26
|
-
/// @return permissionId The permission ID
|
|
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.
|
package/src/structs/JBOwner.sol
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
/// @notice
|
|
5
|
-
/// @custom:member owner
|
|
6
|
-
/// @custom:member projectId
|
|
7
|
-
/// `owner`
|
|
8
|
-
/// @custom:member permissionId The permission ID
|
|
9
|
-
///
|
|
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;
|