@bananapus/ownable-v6 0.0.8 → 0.0.10
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/ADMINISTRATION.md +1 -1
- package/AUDIT_INSTRUCTIONS.md +133 -0
- package/CHANGE_LOG.md +221 -0
- package/RISKS.md +15 -17
- package/STYLE_GUIDE.md +16 -2
- package/USER_JOURNEYS.md +245 -0
- package/package.json +3 -3
- package/src/JBOwnable.sol +4 -1
- package/src/JBOwnableOverrides.sol +12 -7
- package/src/structs/JBOwner.sol +1 -0
- package/test/Ownable.t.sol +39 -30
- package/test/OwnableAttacks.t.sol +23 -18
- package/test/OwnableEdgeCases.t.sol +41 -35
- package/test/OwnableInvariantTests.sol +0 -9
- package/test/handlers/OwnableHandler.sol +1 -4
- package/test/mocks/MockOwnable.sol +1 -1
- package/test/regression/{L65_BurnLockProtection.t.sol → BurnLockProtection.t.sol} +19 -17
- package/test/regression/{L66_ZeroAddressValidation.t.sol → ZeroAddressValidation.t.sol} +15 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import "forge-std/Test.sol";
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
6
6
|
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
7
7
|
|
|
@@ -16,8 +16,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
|
|
|
16
16
|
/// @notice Adversarial security tests for JBOwnable covering edge cases
|
|
17
17
|
/// around dual ownership, permission semantics, and renounced contracts.
|
|
18
18
|
contract OwnableAttacks is Test {
|
|
19
|
-
IJBProjects
|
|
20
|
-
IJBPermissions
|
|
19
|
+
IJBProjects projects;
|
|
20
|
+
IJBPermissions permissions;
|
|
21
21
|
|
|
22
22
|
address alice = makeAddr("alice");
|
|
23
23
|
address bob = makeAddr("bob");
|
|
@@ -33,25 +33,26 @@ contract OwnableAttacks is Test {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function setUp() public {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
permissions = new JBPermissions(address(0));
|
|
37
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// =========================================================================
|
|
41
41
|
// Test 1: Constructor rejects both owner AND projectId set
|
|
42
42
|
// =========================================================================
|
|
43
43
|
function test_bothOwnerAndProjectId_constructorReverts() public {
|
|
44
|
-
uint256 projectId =
|
|
44
|
+
uint256 projectId = projects.createFor(alice);
|
|
45
45
|
|
|
46
46
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
47
|
-
|
|
47
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
48
|
+
new MockOwnable(projects, permissions, bob, uint88(projectId));
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
// =========================================================================
|
|
51
52
|
// Test 2: Renounced contract — protectedMethod always reverts
|
|
52
53
|
// =========================================================================
|
|
53
54
|
function test_renounced_protectedMethodAlwaysReverts() public {
|
|
54
|
-
MockOwnable ownable = new MockOwnable(
|
|
55
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
55
56
|
|
|
56
57
|
// Owner can call.
|
|
57
58
|
vm.prank(alice);
|
|
@@ -82,8 +83,9 @@ contract OwnableAttacks is Test {
|
|
|
82
83
|
/// @notice After any ownership transfer, permissionId should reset to 0.
|
|
83
84
|
/// This prevents stale permission delegation.
|
|
84
85
|
function test_permissionIdResetOnTransfer() public {
|
|
85
|
-
uint256 projectId =
|
|
86
|
-
|
|
86
|
+
uint256 projectId = projects.createFor(alice);
|
|
87
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
88
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
87
89
|
|
|
88
90
|
// Set permission ID.
|
|
89
91
|
vm.prank(alice);
|
|
@@ -105,8 +107,9 @@ contract OwnableAttacks is Test {
|
|
|
105
107
|
// =========================================================================
|
|
106
108
|
/// @notice After transferring project NFT, old owner should lose access.
|
|
107
109
|
function test_staleOwner_afterNFTTransfer() public {
|
|
108
|
-
uint256 projectId =
|
|
109
|
-
|
|
110
|
+
uint256 projectId = projects.createFor(alice);
|
|
111
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
112
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
110
113
|
|
|
111
114
|
// Alice is current owner.
|
|
112
115
|
assertEq(ownable.owner(), alice);
|
|
@@ -115,7 +118,7 @@ contract OwnableAttacks is Test {
|
|
|
115
118
|
|
|
116
119
|
// Transfer project NFT to bob.
|
|
117
120
|
vm.prank(alice);
|
|
118
|
-
|
|
121
|
+
projects.transferFrom(alice, bob, projectId);
|
|
119
122
|
|
|
120
123
|
// Alice should no longer be owner.
|
|
121
124
|
assertEq(ownable.owner(), bob, "Bob should be new owner");
|
|
@@ -135,7 +138,7 @@ contract OwnableAttacks is Test {
|
|
|
135
138
|
// =========================================================================
|
|
136
139
|
/// @notice transferOwnershipToProject with projectId > type(uint88).max should revert.
|
|
137
140
|
function test_transferOwnershipToProject_overflowReverts() public {
|
|
138
|
-
MockOwnable ownable = new MockOwnable(
|
|
141
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
139
142
|
|
|
140
143
|
// type(uint88).max + 1 = 309485009821345068724781056
|
|
141
144
|
uint256 overflowId = uint256(type(uint88).max) + 1;
|
|
@@ -152,11 +155,12 @@ contract OwnableAttacks is Test {
|
|
|
152
155
|
/// doesn't grant access to a different project's JBOwnable.
|
|
153
156
|
function test_rootOnWrongProject_noAccess() public {
|
|
154
157
|
// Create two projects.
|
|
155
|
-
uint256 aliceProject =
|
|
156
|
-
uint256 attackerProject =
|
|
158
|
+
uint256 aliceProject = projects.createFor(alice);
|
|
159
|
+
uint256 attackerProject = projects.createFor(attacker);
|
|
157
160
|
|
|
158
161
|
// Ownable is owned by alice's project.
|
|
159
|
-
|
|
162
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
163
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(aliceProject));
|
|
160
164
|
|
|
161
165
|
// Set permission ID so delegated access is possible.
|
|
162
166
|
vm.prank(alice);
|
|
@@ -167,8 +171,9 @@ contract OwnableAttacks is Test {
|
|
|
167
171
|
rootPerms[0] = 1; // ROOT
|
|
168
172
|
|
|
169
173
|
vm.prank(attacker);
|
|
170
|
-
|
|
174
|
+
permissions.setPermissionsFor(
|
|
171
175
|
attacker,
|
|
176
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
172
177
|
JBPermissionsData({operator: attacker, projectId: uint56(attackerProject), permissionIds: rootPerms})
|
|
173
178
|
);
|
|
174
179
|
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import "forge-std/Test.sol";
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
6
6
|
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
7
|
-
import {JBOwner} from "../src/structs/JBOwner.sol";
|
|
8
7
|
import {IJBOwnable} from "../src/interfaces/IJBOwnable.sol";
|
|
9
8
|
|
|
10
9
|
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
11
|
-
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
12
10
|
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
13
11
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
14
12
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
@@ -18,8 +16,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
|
|
|
18
16
|
/// @notice Edge case and gap tests for JBOwnable: multi-hop NFT transfers,
|
|
19
17
|
/// project-to-project ownership, permissionId lifecycle, and nonexistent projects.
|
|
20
18
|
contract OwnableEdgeCases is Test {
|
|
21
|
-
IJBProjects
|
|
22
|
-
IJBPermissions
|
|
19
|
+
IJBProjects projects;
|
|
20
|
+
IJBPermissions permissions;
|
|
23
21
|
|
|
24
22
|
address alice = makeAddr("alice");
|
|
25
23
|
address bob = makeAddr("bob");
|
|
@@ -36,32 +34,33 @@ contract OwnableEdgeCases is Test {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
function setUp() public {
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
permissions = new JBPermissions(address(0));
|
|
38
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
// =========================================================================
|
|
44
42
|
// Test 1: Multi-hop NFT transfer — ownership follows through A→B→C→D
|
|
45
43
|
// =========================================================================
|
|
46
44
|
function test_multiHopNFTTransfer_ownerFollows() public {
|
|
47
|
-
uint256 projectId =
|
|
48
|
-
|
|
45
|
+
uint256 projectId = projects.createFor(alice);
|
|
46
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
47
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
49
48
|
|
|
50
49
|
assertEq(ownable.owner(), alice);
|
|
51
50
|
|
|
52
51
|
// Transfer NFT: alice → bob
|
|
53
52
|
vm.prank(alice);
|
|
54
|
-
|
|
53
|
+
projects.transferFrom(alice, bob, projectId);
|
|
55
54
|
assertEq(ownable.owner(), bob, "Should follow to bob");
|
|
56
55
|
|
|
57
56
|
// Transfer NFT: bob → charlie
|
|
58
57
|
vm.prank(bob);
|
|
59
|
-
|
|
58
|
+
projects.transferFrom(bob, charlie, projectId);
|
|
60
59
|
assertEq(ownable.owner(), charlie, "Should follow to charlie");
|
|
61
60
|
|
|
62
61
|
// Transfer NFT: charlie → dave
|
|
63
62
|
vm.prank(charlie);
|
|
64
|
-
|
|
63
|
+
projects.transferFrom(charlie, dave, projectId);
|
|
65
64
|
assertEq(ownable.owner(), dave, "Should follow to dave");
|
|
66
65
|
|
|
67
66
|
// dave can call protectedMethod, alice/bob/charlie cannot
|
|
@@ -85,10 +84,11 @@ contract OwnableEdgeCases is Test {
|
|
|
85
84
|
// Test 2: Transfer project → different project
|
|
86
85
|
// =========================================================================
|
|
87
86
|
function test_transferProjectToProject() public {
|
|
88
|
-
uint256 projectA =
|
|
89
|
-
uint256 projectB =
|
|
87
|
+
uint256 projectA = projects.createFor(alice);
|
|
88
|
+
uint256 projectB = projects.createFor(bob);
|
|
90
89
|
|
|
91
|
-
|
|
90
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
91
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
|
|
92
92
|
assertEq(ownable.owner(), alice);
|
|
93
93
|
|
|
94
94
|
// Transfer ownership from project A to project B.
|
|
@@ -112,11 +112,11 @@ contract OwnableEdgeCases is Test {
|
|
|
112
112
|
// Test 3: Full ownership cycle: address → project → address → project
|
|
113
113
|
// =========================================================================
|
|
114
114
|
function test_fullOwnershipCycle() public {
|
|
115
|
-
uint256 projectA =
|
|
116
|
-
uint256 projectB =
|
|
115
|
+
uint256 projectA = projects.createFor(alice);
|
|
116
|
+
uint256 projectB = projects.createFor(bob);
|
|
117
117
|
|
|
118
118
|
// Start with address ownership.
|
|
119
|
-
MockOwnable ownable = new MockOwnable(
|
|
119
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, charlie, 0);
|
|
120
120
|
assertEq(ownable.owner(), charlie);
|
|
121
121
|
|
|
122
122
|
// charlie → project A (alice)
|
|
@@ -137,6 +137,7 @@ contract OwnableEdgeCases is Test {
|
|
|
137
137
|
// Verify jbOwner struct is correct (projectId set, owner zeroed).
|
|
138
138
|
(address storedOwner, uint88 storedProjectId, uint8 storedPermId) = ownable.jbOwner();
|
|
139
139
|
assertEq(storedOwner, address(0), "owner field should be zero in project mode");
|
|
140
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
140
141
|
assertEq(storedProjectId, uint88(projectB), "projectId should be projectB");
|
|
141
142
|
assertEq(storedPermId, 0, "permissionId should be 0");
|
|
142
143
|
}
|
|
@@ -145,9 +146,10 @@ contract OwnableEdgeCases is Test {
|
|
|
145
146
|
// Test 4: permissionId lifecycle through multiple transfers
|
|
146
147
|
// =========================================================================
|
|
147
148
|
function test_permissionIdLifecycle() public {
|
|
148
|
-
uint256 projectA =
|
|
149
|
+
uint256 projectA = projects.createFor(alice);
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
152
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
|
|
151
153
|
|
|
152
154
|
// Set permissionId to 42.
|
|
153
155
|
vm.prank(alice);
|
|
@@ -192,7 +194,7 @@ contract OwnableEdgeCases is Test {
|
|
|
192
194
|
function test_nonOwnerCannotSetPermissionId(address nonOwner) public {
|
|
193
195
|
vm.assume(nonOwner != alice && nonOwner != address(0));
|
|
194
196
|
|
|
195
|
-
MockOwnable ownable = new MockOwnable(
|
|
197
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
196
198
|
|
|
197
199
|
vm.prank(nonOwner);
|
|
198
200
|
vm.expectRevert();
|
|
@@ -204,8 +206,8 @@ contract OwnableEdgeCases is Test {
|
|
|
204
206
|
// =========================================================================
|
|
205
207
|
function test_transferToNonexistentProject_reverts() public {
|
|
206
208
|
// Create one project so count == 1.
|
|
207
|
-
|
|
208
|
-
MockOwnable ownable = new MockOwnable(
|
|
209
|
+
projects.createFor(alice);
|
|
210
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
209
211
|
|
|
210
212
|
// Project 2 doesn't exist.
|
|
211
213
|
vm.prank(alice);
|
|
@@ -223,8 +225,9 @@ contract OwnableEdgeCases is Test {
|
|
|
223
225
|
// transferred, old delegate loses access
|
|
224
226
|
// =========================================================================
|
|
225
227
|
function test_delegatedAccess_lostAfterNFTTransfer() public {
|
|
226
|
-
uint256 projectId =
|
|
227
|
-
|
|
228
|
+
uint256 projectId = projects.createFor(alice);
|
|
229
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
230
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
228
231
|
|
|
229
232
|
// Set permissionId so delegation is possible.
|
|
230
233
|
vm.prank(alice);
|
|
@@ -234,8 +237,10 @@ contract OwnableEdgeCases is Test {
|
|
|
234
237
|
uint8[] memory permIds = new uint8[](1);
|
|
235
238
|
permIds[0] = 42;
|
|
236
239
|
vm.prank(alice);
|
|
237
|
-
|
|
238
|
-
alice,
|
|
240
|
+
permissions.setPermissionsFor(
|
|
241
|
+
alice,
|
|
242
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
243
|
+
JBPermissionsData({operator: charlie, projectId: uint56(projectId), permissionIds: permIds})
|
|
239
244
|
);
|
|
240
245
|
|
|
241
246
|
// Charlie can call protectedMethod (delegated via permissions).
|
|
@@ -244,7 +249,7 @@ contract OwnableEdgeCases is Test {
|
|
|
244
249
|
|
|
245
250
|
// Transfer NFT to bob.
|
|
246
251
|
vm.prank(alice);
|
|
247
|
-
|
|
252
|
+
projects.transferFrom(alice, bob, projectId);
|
|
248
253
|
|
|
249
254
|
// Charlie's delegation was from alice. Now owner is bob.
|
|
250
255
|
// Charlie should lose access because _checkOwner resolves to bob,
|
|
@@ -262,7 +267,7 @@ contract OwnableEdgeCases is Test {
|
|
|
262
267
|
// Test 8: OwnershipTransferred event emitted correctly
|
|
263
268
|
// =========================================================================
|
|
264
269
|
function test_ownershipTransferredEvent() public {
|
|
265
|
-
MockOwnable ownable = new MockOwnable(
|
|
270
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
266
271
|
|
|
267
272
|
// Transfer to bob — expect event.
|
|
268
273
|
vm.expectEmit(true, true, false, true);
|
|
@@ -276,7 +281,7 @@ contract OwnableEdgeCases is Test {
|
|
|
276
281
|
// Test 9: PermissionIdChanged event emitted correctly
|
|
277
282
|
// =========================================================================
|
|
278
283
|
function test_permissionIdChangedEvent() public {
|
|
279
|
-
MockOwnable ownable = new MockOwnable(
|
|
284
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
280
285
|
|
|
281
286
|
vm.expectEmit(true, true, false, true);
|
|
282
287
|
emit IJBOwnable.PermissionIdChanged(42, alice);
|
|
@@ -291,8 +296,8 @@ contract OwnableEdgeCases is Test {
|
|
|
291
296
|
function testFuzz_transferToProject(address projectOwner) public isNotContract(projectOwner) {
|
|
292
297
|
vm.assume(projectOwner != address(0));
|
|
293
298
|
|
|
294
|
-
uint256 projectId =
|
|
295
|
-
MockOwnable ownable = new MockOwnable(
|
|
299
|
+
uint256 projectId = projects.createFor(projectOwner);
|
|
300
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
296
301
|
|
|
297
302
|
vm.prank(alice);
|
|
298
303
|
ownable.transferOwnershipToProject(projectId);
|
|
@@ -302,6 +307,7 @@ contract OwnableEdgeCases is Test {
|
|
|
302
307
|
// Verify jbOwner struct.
|
|
303
308
|
(address storedOwner, uint88 storedProjectId,) = ownable.jbOwner();
|
|
304
309
|
assertEq(storedOwner, address(0), "stored owner should be zero");
|
|
310
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
305
311
|
assertEq(storedProjectId, uint88(projectId), "stored projectId should match");
|
|
306
312
|
}
|
|
307
313
|
|
|
@@ -311,8 +317,8 @@ contract OwnableEdgeCases is Test {
|
|
|
311
317
|
/// @notice After renouncing, no one can call transferOwnership, transferOwnershipToProject,
|
|
312
318
|
/// setPermissionId, or renounceOwnership again.
|
|
313
319
|
function test_renouncedContract_cannotReclaim() public {
|
|
314
|
-
uint256 projectId =
|
|
315
|
-
MockOwnable ownable = new MockOwnable(
|
|
320
|
+
uint256 projectId = projects.createFor(alice);
|
|
321
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
316
322
|
|
|
317
323
|
vm.prank(alice);
|
|
318
324
|
ownable.renounceOwnership();
|
|
@@ -349,7 +355,7 @@ contract OwnableEdgeCases is Test {
|
|
|
349
355
|
/// NOT ERC2771Context. This test documents that a trusted forwarder
|
|
350
356
|
/// appending a sender address to calldata does NOT affect _checkOwner.
|
|
351
357
|
function test_noERC2771_trustedForwarderHasNoEffect() public {
|
|
352
|
-
MockOwnable ownable = new MockOwnable(
|
|
358
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
353
359
|
|
|
354
360
|
// Simulate what a trusted forwarder would do: call with alice's address
|
|
355
361
|
// appended to calldata. Since JBOwnable uses plain Context, this has no effect.
|
|
@@ -4,15 +4,6 @@ pragma solidity ^0.8.26;
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {OwnableHandler} from "./handlers/OwnableHandler.sol";
|
|
6
6
|
|
|
7
|
-
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
8
|
-
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
9
|
-
import {JBOwner} from "../src/structs/JBOwner.sol";
|
|
10
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.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 {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
14
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
15
|
-
|
|
16
7
|
contract OwnableInvariantTests is Test {
|
|
17
8
|
OwnableHandler handler;
|
|
18
9
|
|
|
@@ -5,12 +5,9 @@ pragma solidity ^0.8.26;
|
|
|
5
5
|
import {CommonBase} from "forge-std/Base.sol";
|
|
6
6
|
import {StdCheats} from "forge-std/StdCheats.sol";
|
|
7
7
|
import {StdUtils} from "forge-std/StdUtils.sol";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {MockOwnable, JBOwnableOverrides} from "../mocks/MockOwnable.sol";
|
|
8
|
+
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
11
9
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
10
|
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
13
|
-
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
14
11
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
15
12
|
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
16
13
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import {JBOwnable
|
|
4
|
+
import {JBOwnable} from "../../src/JBOwnable.sol";
|
|
5
5
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
6
6
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
7
7
|
|
|
@@ -3,7 +3,6 @@ pragma solidity ^0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
6
|
-
import {JBOwnableOverrides} from "../../src/JBOwnableOverrides.sol";
|
|
7
6
|
|
|
8
7
|
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
9
8
|
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
@@ -11,34 +10,35 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
|
|
|
11
10
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
11
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
13
12
|
|
|
14
|
-
/// @title
|
|
13
|
+
/// @title BurnLockProtection
|
|
15
14
|
/// @notice Verifies that if a project NFT is burned/invalidated,
|
|
16
15
|
/// owner() returns address(0) and _checkOwner() reverts gracefully instead of
|
|
17
16
|
/// permanently locking the contract with an unrecoverable revert.
|
|
18
|
-
contract
|
|
19
|
-
IJBProjects
|
|
20
|
-
IJBPermissions
|
|
17
|
+
contract BurnLockProtection is Test {
|
|
18
|
+
IJBProjects projects;
|
|
19
|
+
IJBPermissions permissions;
|
|
21
20
|
|
|
22
21
|
address alice = makeAddr("alice");
|
|
23
22
|
address bob = makeAddr("bob");
|
|
24
23
|
|
|
25
24
|
function setUp() public {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
permissions = new JBPermissions(address(0));
|
|
26
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/// @notice When a project NFT is burned (simulated via mockCallRevert), owner() should
|
|
31
30
|
/// return address(0) instead of reverting — contract degrades to "renounced" state.
|
|
32
31
|
function test_burnedProjectNFT_ownerReturnsZero() public {
|
|
33
|
-
uint256 projectId =
|
|
34
|
-
|
|
32
|
+
uint256 projectId = projects.createFor(alice);
|
|
33
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
34
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
35
35
|
|
|
36
36
|
// Verify normal operation first.
|
|
37
37
|
assertEq(ownable.owner(), alice, "Owner should be alice before burn");
|
|
38
38
|
|
|
39
39
|
// Simulate project NFT burn by making ownerOf revert for this projectId.
|
|
40
40
|
vm.mockCallRevert(
|
|
41
|
-
address(
|
|
41
|
+
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
// After burn, owner() should return address(0) — NOT revert.
|
|
@@ -50,8 +50,9 @@ contract L65_BurnLockProtection is Test {
|
|
|
50
50
|
/// Unauthorized error (not an unrecoverable ownerOf revert), making the contract
|
|
51
51
|
/// behave as if ownership was renounced.
|
|
52
52
|
function test_burnedProjectNFT_checkOwnerRevertsGracefully() public {
|
|
53
|
-
uint256 projectId =
|
|
54
|
-
|
|
53
|
+
uint256 projectId = projects.createFor(alice);
|
|
54
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
55
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
55
56
|
|
|
56
57
|
// Alice can call the protected method before burn.
|
|
57
58
|
vm.prank(alice);
|
|
@@ -59,7 +60,7 @@ contract L65_BurnLockProtection is Test {
|
|
|
59
60
|
|
|
60
61
|
// Simulate project NFT burn.
|
|
61
62
|
vm.mockCallRevert(
|
|
62
|
-
address(
|
|
63
|
+
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
|
|
63
64
|
);
|
|
64
65
|
|
|
65
66
|
// After burn, nobody can call protected methods — but the revert is graceful
|
|
@@ -75,7 +76,7 @@ contract L65_BurnLockProtection is Test {
|
|
|
75
76
|
|
|
76
77
|
/// @notice Address-based ownership is unaffected by the try-catch change.
|
|
77
78
|
function test_addressBasedOwnership_unaffectedByTryCatch() public {
|
|
78
|
-
MockOwnable ownable = new MockOwnable(
|
|
79
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
79
80
|
|
|
80
81
|
assertEq(ownable.owner(), alice, "Owner should be alice");
|
|
81
82
|
|
|
@@ -93,14 +94,15 @@ contract L65_BurnLockProtection is Test {
|
|
|
93
94
|
|
|
94
95
|
/// @notice Normal project-based ownership still works correctly after the fix.
|
|
95
96
|
function test_normalProjectOwnership_stillWorks() public {
|
|
96
|
-
uint256 projectId =
|
|
97
|
-
|
|
97
|
+
uint256 projectId = projects.createFor(alice);
|
|
98
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
99
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
98
100
|
|
|
99
101
|
assertEq(ownable.owner(), alice);
|
|
100
102
|
|
|
101
103
|
// Transfer project NFT.
|
|
102
104
|
vm.prank(alice);
|
|
103
|
-
|
|
105
|
+
projects.transferFrom(alice, bob, projectId);
|
|
104
106
|
|
|
105
107
|
assertEq(ownable.owner(), bob, "Owner should follow project NFT transfer");
|
|
106
108
|
|
|
@@ -10,19 +10,19 @@ import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
|
10
10
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
11
11
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
12
|
|
|
13
|
-
/// @title
|
|
14
|
-
/// @notice Verifies that deploying with a zero-address
|
|
13
|
+
/// @title ZeroAddressValidation
|
|
14
|
+
/// @notice Verifies that deploying with a zero-address projects
|
|
15
15
|
/// contract and a non-zero projectId reverts at construction time, preventing
|
|
16
16
|
/// permanently broken project-based ownership.
|
|
17
|
-
contract
|
|
18
|
-
IJBProjects
|
|
19
|
-
IJBPermissions
|
|
17
|
+
contract ZeroAddressValidation is Test {
|
|
18
|
+
IJBProjects projects;
|
|
19
|
+
IJBPermissions permissions;
|
|
20
20
|
|
|
21
21
|
address alice = makeAddr("alice");
|
|
22
22
|
|
|
23
23
|
function setUp() public {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
permissions = new JBPermissions(address(0));
|
|
25
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/// @notice Deploying with projects=address(0) and non-zero projectId must revert.
|
|
@@ -30,7 +30,7 @@ contract L66_ZeroAddressValidation is Test {
|
|
|
30
30
|
vm.expectRevert(
|
|
31
31
|
abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
|
|
32
32
|
);
|
|
33
|
-
new MockOwnable(IJBProjects(address(0)),
|
|
33
|
+
new MockOwnable(IJBProjects(address(0)), permissions, address(0), uint88(1));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/// @notice Fuzz: any non-zero projectId with projects=address(0) must revert.
|
|
@@ -40,27 +40,28 @@ contract L66_ZeroAddressValidation is Test {
|
|
|
40
40
|
vm.expectRevert(
|
|
41
41
|
abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
|
|
42
42
|
);
|
|
43
|
-
new MockOwnable(IJBProjects(address(0)),
|
|
43
|
+
new MockOwnable(IJBProjects(address(0)), permissions, address(0), projectId);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/// @notice Deploying with projects=address(0) and projectId=0 (address-based ownership)
|
|
47
47
|
/// should NOT revert for this error — it's valid as long as initialOwner != address(0).
|
|
48
48
|
function test_zeroProjectsWithAddressOwnership_succeeds() public {
|
|
49
49
|
// This is valid: address-based ownership with projects=address(0).
|
|
50
|
-
MockOwnable ownable = new MockOwnable(IJBProjects(address(0)),
|
|
50
|
+
MockOwnable ownable = new MockOwnable(IJBProjects(address(0)), permissions, alice, uint88(0));
|
|
51
51
|
assertEq(ownable.owner(), alice, "Owner should be alice with address-based ownership");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/// @notice Normal deployment with valid
|
|
54
|
+
/// @notice Normal deployment with valid projects contract and projectId succeeds.
|
|
55
55
|
function test_validProjectsWithProjectId_succeeds() public {
|
|
56
|
-
uint256 projectId =
|
|
57
|
-
|
|
56
|
+
uint256 projectId = projects.createFor(alice);
|
|
57
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
58
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
58
59
|
assertEq(ownable.owner(), alice, "Owner should be alice via project NFT");
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/// @notice The existing check for both zero owner and zero projectId is still enforced.
|
|
62
63
|
function test_bothZero_stillReverts() public {
|
|
63
64
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
64
|
-
new MockOwnable(
|
|
65
|
+
new MockOwnable(projects, permissions, address(0), uint88(0));
|
|
65
66
|
}
|
|
66
67
|
}
|