@bananapus/ownable-v6 0.0.22 → 0.0.24

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/USER_JOURNEYS.md DELETED
@@ -1,117 +0,0 @@
1
- # User Journeys
2
-
3
- ## Repo Purpose
4
-
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).
6
-
7
- ## Primary Actors
8
-
9
- - protocol or product teams that want `onlyOwner` to follow a project NFT
10
- - operators who need owner-like access without receiving the project itself
11
- - auditors checking whether delegated owner semantics strand or over-grant authority
12
-
13
- ## Key Surfaces
14
-
15
- - `JBOwnable`: `Ownable`-style adapter whose owner follows a Juicebox project
16
- - `JBOwnableOverrides`: extension that lets a project-scoped permission satisfy `onlyOwner`
17
- - `owner()`, `transferOwnership(...)`, `transferOwnershipToProject(...)`, `setPermissionId(...)`: core ownership-resolution and migration paths
18
-
19
- ## Journey 1: Give A Contract To A Juicebox Project Instead Of A Wallet
20
-
21
- **Actor:** downstream contract author.
22
-
23
- **Intent:** make a contract follow Juicebox project ownership instead of a fixed EOA or multisig.
24
-
25
- **Preconditions**
26
- - the downstream contract wants `onlyOwner` ergonomics
27
- - a project ID and `JBProjects` dependency are already known
28
-
29
- **Main Flow**
30
- 1. Inherit `JBOwnable` or `JBOwnableOverrides`.
31
- 2. Initialize ownership with the relevant project ID and `JBProjects` reference.
32
- 3. Let `owner()` resolve through the current project NFT holder instead of a fixed address.
33
-
34
- **Failure Modes**
35
- - the contract assumes ordinary `Ownable` transfer semantics after adopting project-based ownership
36
- - the wrong project ID is configured
37
- - reviewers ignore the adapter and audit the downstream contract as if `owner` were fixed
38
-
39
- **Postconditions**
40
- - `owner()` now resolves through the configured project NFT instead of a fixed wallet
41
-
42
- ## Journey 2: Delegate Owner-Level Access To Operators
43
-
44
- **Actor:** current project owner.
45
-
46
- **Intent:** let an operator satisfy `onlyOwner` for one contract without transferring the project.
47
-
48
- **Preconditions**
49
- - the downstream contract uses `JBOwnableOverrides`
50
- - the team has chosen the permission ID that should count as delegated owner access
51
-
52
- **Main Flow**
53
- 1. Choose the permission ID the downstream contract should respect.
54
- 2. Grant that permission through `JBPermissions`.
55
- 3. `JBOwnableOverrides` treats the operator as satisfying `onlyOwner` for that contract.
56
-
57
- **Failure Modes**
58
- - teams grant a broader permission than intended
59
- - downstream reviewers forget that `onlyOwner` may resolve through permissions instead of direct ownership
60
- - operators keep stale permissions after governance changes
61
-
62
- **Postconditions**
63
- - the chosen operator can satisfy `onlyOwner` without receiving direct ownership of the project or contract
64
-
65
- ## Journey 3: Change The Delegated Permission ID Without Changing Ownership
66
-
67
- **Actor:** current effective owner.
68
-
69
- **Intent:** rotate delegated owner policy without changing the underlying owner.
70
-
71
- **Preconditions**
72
- - the contract already uses `JBOwnableOverrides`
73
- - all operators who need continued access can be regranted under the new permission ID
74
-
75
- **Main Flow**
76
- 1. Update the permission ID the adapter treats as owner-equivalent with `setPermissionId(...)`.
77
- 2. Re-grant the new permission where needed.
78
- 3. Re-audit operator assumptions because the old permission no longer satisfies `onlyOwner`.
79
-
80
- **Failure Modes**
81
- - operator access disappears unintentionally after a permission-ID rotation
82
- - teams forget that old delegations stop working immediately
83
-
84
- **Postconditions**
85
- - the adapter now resolves delegated owner access through the new permission ID only
86
-
87
- ## Journey 4: Transfer Or Burn Ownership Deliberately
88
-
89
- **Actor:** current effective owner.
90
-
91
- **Intent:** move or remove control with full awareness of the consequences.
92
-
93
- **Preconditions**
94
- - the team understands whether admin recovery should remain possible
95
- - downstream integrations can tolerate the new owner model
96
-
97
- **Main Flow**
98
- 1. Use `transferOwnership(...)` for an address owner or `transferOwnershipToProject(...)` for a project owner.
99
- 2. Re-establish delegated permission policy if the new owner still wants operators.
100
- 3. Renounce only when permanent admin loss is intentional.
101
-
102
- **Failure Modes**
103
- - ownership is burned even though the downstream contract still needs administration
104
- - teams forget that delegated permissions reset across ownership changes
105
-
106
- **Postconditions**
107
- - control moves to the chosen address or project, or is intentionally removed
108
-
109
- ## Trust Boundaries
110
-
111
- - this repo trusts `JBProjects` for project ownership and `JBPermissions` for delegated authority
112
- - downstream contracts still need their own audit because this adapter changes who satisfies `onlyOwner`
113
-
114
- ## Hand-Offs
115
-
116
- - Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for the project-NFT and permission machinery this adapter depends on.
117
- - Use [nana-permission-ids-v6](../nana-permission-ids-v6/USER_JOURNEYS.md) if you need the shared numeric permission vocabulary for delegated `onlyOwner` checks.
@@ -1,10 +0,0 @@
1
- {
2
- "detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
3
- "exclude_informational": true,
4
- "exclude_low": false,
5
- "exclude_medium": false,
6
- "exclude_high": false,
7
- "disable_color": false,
8
- "filter_paths": "(mocks/|test/|node_modules/|lib/)",
9
- "legacy_ast": false
10
- }
@@ -1,45 +0,0 @@
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 {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
9
- import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
10
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
11
- import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
12
-
13
- contract CodexUnmintedProjectHijackTest is Test {
14
- IJBProjects internal projects;
15
- IJBPermissions internal permissions;
16
-
17
- address internal deployer = makeAddr("deployer");
18
- address internal attacker = makeAddr("attacker");
19
- address internal intendedOwner = makeAddr("intendedOwner");
20
-
21
- function setUp() public {
22
- permissions = new JBPermissions(address(0));
23
- projects = new JBProjects(address(this), address(0), address(0));
24
- }
25
-
26
- function test_unmintedProjectOwnerCanBeHijackedByFirstMinter() external {
27
- vm.prank(deployer);
28
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), 1);
29
-
30
- assertEq(ownable.owner(), address(0), "owner should be unresolved before project 1 exists");
31
-
32
- vm.prank(attacker);
33
- uint256 hijackedProjectId = projects.createFor(attacker);
34
-
35
- assertEq(hijackedProjectId, 1, "attacker should receive the configured project ID");
36
- assertEq(ownable.owner(), attacker, "first minter becomes owner of the ownable contract");
37
-
38
- vm.prank(attacker);
39
- ownable.protectedMethod();
40
-
41
- vm.prank(intendedOwner);
42
- vm.expectRevert();
43
- ownable.protectedMethod();
44
- }
45
- }
@@ -1,383 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {Test} from "forge-std/Test.sol";
5
- import {MockOwnable} from "./mocks/MockOwnable.sol";
6
- import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
7
-
8
- import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
9
- import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
10
- import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
11
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
12
- import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
13
- import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
14
-
15
- contract OwnableTest is Test {
16
- IJBProjects projects;
17
- IJBPermissions permissions;
18
-
19
- modifier isNotContract(address a) {
20
- uint256 size;
21
- assembly {
22
- size := extcodesize(a)
23
- }
24
- vm.assume(size == 0);
25
- _;
26
- }
27
-
28
- function setUp() public {
29
- // Deploy the permissions contract.
30
- permissions = new JBPermissions(address(0));
31
- // Deploy the projects contract.
32
- projects = new JBProjects(address(123), address(0), address(0));
33
- }
34
-
35
- function testDeployerDoesNotBecomeOwner(address deployer, address owner) public isNotContract(owner) {
36
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
37
- vm.assume(owner != address(0));
38
-
39
- vm.prank(deployer);
40
- MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
41
-
42
- assertEq(owner, ownable.owner(), "Deployer did not become the owner.");
43
- }
44
-
45
- function testJBOwnableFollowsTheProjectOwner(
46
- address projectOwner,
47
- address newProjectOwner
48
- )
49
- public
50
- isNotContract(projectOwner)
51
- isNotContract(newProjectOwner)
52
- {
53
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
54
- vm.assume(projectOwner != address(0));
55
- // Can't transfer ownership to the zero address.
56
- vm.assume(newProjectOwner != address(0));
57
-
58
- // Create a project for the owner.
59
- uint256 projectId = projects.createFor(projectOwner);
60
-
61
- // Create the `Ownable` contract.
62
- // forge-lint: disable-next-line(unsafe-typecast)
63
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
64
-
65
- // Make sure the deployer owns it.
66
- assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
67
-
68
- // Transfer the project's ownership.
69
- vm.prank(projectOwner);
70
- projects.transferFrom(projectOwner, newProjectOwner, projectId);
71
-
72
- // Make sure the `Ownable` contract has also been transferred to the new project owner.
73
- assertEq(newProjectOwner, ownable.owner(), "Ownable did not follow the Project owner.");
74
- }
75
-
76
- function testBasicOwnable(
77
- address projectOwner,
78
- address newOwnableOwner
79
- )
80
- public
81
- isNotContract(projectOwner)
82
- isNotContract(newOwnableOwner)
83
- {
84
- // Ownership can't be transferred to the 0 address. To transfer to the 0 address, ownership must be renounced.
85
- vm.assume(newOwnableOwner != address(0));
86
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
87
- vm.assume(projectOwner != address(0));
88
-
89
- // Create a project for the owner.
90
- uint256 _projectId = projects.createFor(projectOwner);
91
-
92
- // Create the `Ownable` contract.
93
- // forge-lint: disable-next-line(unsafe-typecast)
94
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
95
-
96
- // Make sure the project owner owns it.
97
- assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
98
-
99
- // We now stop using it as a `JBOwnable` and start using it like a basic `Ownable`.
100
- vm.prank(projectOwner);
101
- ownable.transferOwnership(newOwnableOwner);
102
- // Make sure it was transferred to the new owner.
103
- assertEq(newOwnableOwner, ownable.owner());
104
- // Sanity check to make sure it only the `Ownable` changed, and that the project did not.
105
- assertEq(projects.ownerOf(_projectId), projectOwner);
106
- }
107
-
108
- function testCantTransferToProjectZero(address owner) public {
109
- vm.assume(owner != address(0));
110
- vm.startPrank(owner);
111
-
112
- // Create the `Ownable` contract.
113
- MockOwnable ownable = new MockOwnable(projects, permissions, owner, 0);
114
-
115
- vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
116
-
117
- // Transfer ownership to project ID 0 (should revert).
118
- ownable.transferOwnershipToProject(0);
119
- vm.stopPrank();
120
- }
121
-
122
- function testCantTransferToAddressZero(address owner) public {
123
- vm.assume(owner != address(0));
124
- vm.startPrank(owner);
125
-
126
- // Create the `Ownable` contract.
127
- MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
128
-
129
- vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
130
-
131
- // Transfer ownership to the 0 address (should revert).
132
- ownable.transferOwnership(address(0));
133
- vm.stopPrank();
134
- }
135
-
136
- function testOwnableFollowsProjectOwner(
137
- address projectOwner,
138
- address newProjectOwner
139
- )
140
- public
141
- isNotContract(projectOwner)
142
- isNotContract(newProjectOwner)
143
- {
144
- vm.assume(projectOwner != newProjectOwner);
145
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
146
- vm.assume(projectOwner != address(0));
147
- vm.assume(newProjectOwner != address(0));
148
-
149
- // Create a project for the owner.
150
- uint256 _projectId = projects.createFor(projectOwner);
151
-
152
- // Create the `Ownable` contract.
153
- // forge-lint: disable-next-line(unsafe-typecast)
154
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
155
-
156
- // Make sure the project owner owns it.
157
- assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
158
-
159
- // Transfer the project ownership.
160
- vm.prank(projectOwner);
161
- projects.transferFrom(projectOwner, newProjectOwner, _projectId);
162
- assertEq(projects.ownerOf(_projectId), newProjectOwner);
163
-
164
- // Make sure the `Ownable` contract has also been transferred to the new project owner.
165
- assertEq(newProjectOwner, ownable.owner());
166
- }
167
-
168
- function testOwnableOwnerCanRennounce(address deployer, address owner) public {
169
- vm.assume(owner != address(0));
170
- vm.assume(deployer != owner);
171
-
172
- // Create the `Ownable` contract.
173
- MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
174
-
175
- // Transfer ownership to the project owner.
176
- vm.prank(owner);
177
- ownable.transferOwnership(owner);
178
- assertEq(owner, ownable.owner(), "Deployer is not the owner.");
179
-
180
- // Renounce the ownership.
181
- vm.prank(owner);
182
- ownable.renounceOwnership();
183
- assertEq(address(0), ownable.owner(), "Owner was not renounced.");
184
- }
185
-
186
- function testJBOwnableOwnerCanRennounce(address deployer, address projectOwner) public isNotContract(projectOwner) {
187
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
188
- vm.assume(projectOwner != address(0));
189
-
190
- // Create a project for the owner.
191
- uint256 _projectId = projects.createFor(projectOwner);
192
-
193
- // Create the `Ownable` contract.
194
- vm.prank(deployer);
195
- // forge-lint: disable-next-line(unsafe-typecast)
196
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
197
-
198
- // Renounce the ownership.
199
- vm.prank(projectOwner);
200
- ownable.renounceOwnership();
201
- assertEq(address(0), ownable.owner(), "Owner was not renounced.");
202
- }
203
-
204
- function testJBOwnablePermissions(
205
- address projectOwner,
206
- address callerAddress,
207
- uint8 requiredPermissionId,
208
- uint8[] memory permissionIdsToGrant
209
- )
210
- public
211
- isNotContract(projectOwner)
212
- {
213
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
214
- vm.assume(projectOwner != address(0) && callerAddress != projectOwner);
215
- requiredPermissionId = uint8(bound(uint256(requiredPermissionId), 1, 255));
216
-
217
- // Truncate array instead of rejecting to avoid exceeding max_test_rejects.
218
- if (permissionIdsToGrant.length > 4) {
219
- assembly {
220
- mstore(permissionIdsToGrant, 4)
221
- }
222
- }
223
-
224
- // Create a project for the owner.
225
- uint256 _projectId = projects.createFor(projectOwner);
226
-
227
- // Create the `Ownable` contract.
228
- // forge-lint: disable-next-line(unsafe-typecast)
229
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
230
-
231
- // Set the required permission.
232
- vm.prank(projectOwner);
233
- ownable.setPermissionId(requiredPermissionId);
234
-
235
- // Attempt to call the protected method without permission.
236
- vm.expectRevert(
237
- abi.encodeWithSelector(
238
- JBPermissioned.JBPermissioned_Unauthorized.selector,
239
- projectOwner,
240
- callerAddress,
241
- _projectId,
242
- requiredPermissionId
243
- )
244
- );
245
- vm.prank(callerAddress);
246
- ownable.protectedMethod();
247
-
248
- // Give permission.
249
- bool _shouldHavePermission;
250
- uint8[] memory _permissionIds = new uint8[](permissionIdsToGrant.length);
251
- for (uint256 i; i < permissionIdsToGrant.length; i++) {
252
- permissionIdsToGrant[i] = uint8(bound(uint256(permissionIdsToGrant[i]), 1, 255));
253
- // Check if the permission we need is in the permissions to grant, including if it's ROOT.
254
- if (permissionIdsToGrant[i] == requiredPermissionId || permissionIdsToGrant[i] == 1) {
255
- _shouldHavePermission = true;
256
- }
257
- _permissionIds[i] = permissionIdsToGrant[i];
258
- }
259
-
260
- // The owner gives permission to the caller.
261
- vm.prank(projectOwner);
262
- permissions.setPermissionsFor(
263
- projectOwner,
264
- // forge-lint: disable-next-line(unsafe-typecast)
265
- JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
266
- );
267
-
268
- if (!_shouldHavePermission) {
269
- vm.expectRevert(
270
- abi.encodeWithSelector(
271
- JBPermissioned.JBPermissioned_Unauthorized.selector,
272
- projectOwner,
273
- callerAddress,
274
- _projectId,
275
- requiredPermissionId
276
- )
277
- );
278
- }
279
-
280
- vm.prank(callerAddress);
281
- ownable.protectedMethod();
282
- }
283
-
284
- function testJBOwnablePermissionsRequiredModifier(
285
- address projectOwner,
286
- address callerAddress,
287
- uint8 requiredPermissionId,
288
- uint8[] memory permissionIdsToGrant
289
- )
290
- public
291
- isNotContract(projectOwner)
292
- {
293
- // `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
294
- vm.assume(projectOwner != address(0) && callerAddress != projectOwner);
295
- requiredPermissionId = uint8(bound(uint256(requiredPermissionId), 1, 255));
296
-
297
- // Truncate array instead of rejecting to avoid exceeding max_test_rejects.
298
- if (permissionIdsToGrant.length > 4) {
299
- assembly {
300
- mstore(permissionIdsToGrant, 4)
301
- }
302
- }
303
-
304
- // Create a project for the owner.
305
- uint256 _projectId = projects.createFor(projectOwner);
306
-
307
- // Create the `Ownable` contract.
308
- // forge-lint: disable-next-line(unsafe-typecast)
309
- MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
310
-
311
- // Set the permission that is required.
312
- ownable.setPermission(requiredPermissionId);
313
-
314
- // Attempt to call the protected method without permission.
315
- vm.expectRevert(
316
- abi.encodeWithSelector(
317
- JBPermissioned.JBPermissioned_Unauthorized.selector,
318
- projectOwner,
319
- callerAddress,
320
- _projectId,
321
- requiredPermissionId
322
- )
323
- );
324
- vm.prank(callerAddress);
325
- ownable.protectedMethodWithRequirePermission();
326
-
327
- // Give permission.
328
- bool _shouldHavePermission;
329
- uint8[] memory _permissionIds = new uint8[](permissionIdsToGrant.length);
330
- for (uint256 i; i < permissionIdsToGrant.length; i++) {
331
- permissionIdsToGrant[i] = uint8(bound(uint256(permissionIdsToGrant[i]), 1, 255));
332
- // Check if the permission we need is in the permissions to grant, including if it's ROOT.
333
- if (permissionIdsToGrant[i] == requiredPermissionId || permissionIdsToGrant[i] == 1) {
334
- _shouldHavePermission = true;
335
- }
336
- _permissionIds[i] = permissionIdsToGrant[i];
337
- }
338
-
339
- // The owner gives permission to the caller.
340
- vm.prank(projectOwner);
341
- permissions.setPermissionsFor(
342
- projectOwner,
343
- // forge-lint: disable-next-line(unsafe-typecast)
344
- JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
345
- );
346
-
347
- if (!_shouldHavePermission) {
348
- vm.expectRevert(
349
- abi.encodeWithSelector(
350
- JBPermissioned.JBPermissioned_Unauthorized.selector,
351
- projectOwner,
352
- callerAddress,
353
- _projectId,
354
- requiredPermissionId
355
- )
356
- );
357
- }
358
-
359
- vm.prank(callerAddress);
360
- ownable.protectedMethodWithRequirePermission();
361
- }
362
-
363
- function testCantConfigureOwnerAndProject(address owner, address projectOwner) public isNotContract(projectOwner) {
364
- vm.assume(owner != address(0) && projectOwner != address(0));
365
-
366
- // Create a project for the owner.
367
- uint256 _projectId = projects.createFor(projectOwner);
368
-
369
- // Should revert because we set both a owner and a projectOwner
370
- vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
371
-
372
- // Create the `Ownable` contract.
373
- // forge-lint: disable-next-line(unsafe-typecast)
374
- new MockOwnable(projects, permissions, address(owner), uint88(_projectId));
375
- }
376
-
377
- function testCantInitializeAsRenounced() public {
378
- // Should revert because we set both a owner and a projectOwner
379
- vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
380
- // Create the `Ownable` contract.
381
- new MockOwnable(projects, permissions, address(0), uint88(0));
382
- }
383
- }