@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 +9 -6
- package/package.json +3 -3
- package/src/JBOwnable.sol +2 -6
- package/src/JBOwnableOverrides.sol +51 -24
- package/src/interfaces/IJBOwnable.sol +4 -3
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
|
|
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/
|
|
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.
|
|
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.
|
|
21
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
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
|
-
|
|
71
|
-
|
|
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).
|
|
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
|
|
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
|
-
//
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
178
|
-
/// @dev Can only be called by the current owner.
|
|
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
|
-
|
|
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).
|
|
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
|
|
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
|
|