@bananapus/ownable-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +51 -0
- package/SKILLS.md +62 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +12 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +131 -0
- package/docs/src/SUMMARY.md +9 -0
- package/docs/src/src/JBOwnable.sol/contract.JBOwnable.md +71 -0
- package/docs/src/src/JBOwnableOverrides.sol/abstract.JBOwnableOverrides.md +216 -0
- package/docs/src/src/README.md +7 -0
- package/docs/src/src/interfaces/IJBOwnable.sol/interface.IJBOwnable.md +67 -0
- package/docs/src/src/interfaces/README.md +4 -0
- package/docs/src/src/structs/JBOwner.sol/struct.JBOwner.md +23 -0
- package/docs/src/src/structs/README.md +4 -0
- package/foundry.toml +12 -0
- package/package.json +17 -0
- package/slither-ci.config.json +10 -0
- package/src/JBOwnable.sol +76 -0
- package/src/JBOwnableOverrides.sol +203 -0
- package/src/interfaces/IJBOwnable.sol +38 -0
- package/src/structs/JBOwner.sol +14 -0
- package/test/Ownable.t.sol +374 -0
- package/test/OwnableAttacks.t.sol +185 -0
- package/test/OwnableInvariantTests.sol +57 -0
- package/test/handlers/OwnableHandler.sol +78 -0
- package/test/mocks/MockOwnable.sol +45 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import "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
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
|
|
63
|
+
|
|
64
|
+
// Make sure the deployer owns it.
|
|
65
|
+
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
66
|
+
|
|
67
|
+
// Transfer the project's ownership.
|
|
68
|
+
vm.prank(projectOwner);
|
|
69
|
+
PROJECTS.transferFrom(projectOwner, newProjectOwner, projectId);
|
|
70
|
+
|
|
71
|
+
// Make sure the `Ownable` contract has also been transferred to the new project owner.
|
|
72
|
+
assertEq(newProjectOwner, ownable.owner(), "Ownable did not follow the Project owner.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function testBasicOwnable(
|
|
76
|
+
address projectOwner,
|
|
77
|
+
address newOwnableOwner
|
|
78
|
+
)
|
|
79
|
+
public
|
|
80
|
+
isNotContract(projectOwner)
|
|
81
|
+
isNotContract(newOwnableOwner)
|
|
82
|
+
{
|
|
83
|
+
// Ownership can't be transferred to the 0 address. To transfer to the 0 address, ownership must be renounced.
|
|
84
|
+
vm.assume(newOwnableOwner != address(0));
|
|
85
|
+
// `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
|
|
86
|
+
vm.assume(projectOwner != address(0));
|
|
87
|
+
|
|
88
|
+
// Create a project for the owner.
|
|
89
|
+
uint256 _projectId = PROJECTS.createFor(projectOwner);
|
|
90
|
+
|
|
91
|
+
// Create the `Ownable` contract.
|
|
92
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
|
|
93
|
+
|
|
94
|
+
// Make sure the project owner owns it.
|
|
95
|
+
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
96
|
+
|
|
97
|
+
// We now stop using it as a `JBOwnable` and start using it like a basic `Ownable`.
|
|
98
|
+
vm.prank(projectOwner);
|
|
99
|
+
ownable.transferOwnership(newOwnableOwner);
|
|
100
|
+
// Make sure it was transferred to the new owner.
|
|
101
|
+
assertEq(newOwnableOwner, ownable.owner());
|
|
102
|
+
// Sanity check to make sure it only the `Ownable` changed, and that the project did not.
|
|
103
|
+
assertEq(PROJECTS.ownerOf(_projectId), projectOwner);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function testCantTransferToProjectZero(address owner) public {
|
|
107
|
+
vm.assume(owner != address(0));
|
|
108
|
+
vm.startPrank(owner);
|
|
109
|
+
|
|
110
|
+
// Create the `Ownable` contract.
|
|
111
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, 0);
|
|
112
|
+
|
|
113
|
+
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
114
|
+
|
|
115
|
+
// Transfer ownership to project ID 0 (should revert).
|
|
116
|
+
ownable.transferOwnershipToProject(0);
|
|
117
|
+
vm.stopPrank();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function testCantTransferToAddressZero(address owner) public {
|
|
121
|
+
vm.assume(owner != address(0));
|
|
122
|
+
vm.startPrank(owner);
|
|
123
|
+
|
|
124
|
+
// Create the `Ownable` contract.
|
|
125
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, uint88(0));
|
|
126
|
+
|
|
127
|
+
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
128
|
+
|
|
129
|
+
// Transfer ownership to the 0 address (should revert).
|
|
130
|
+
ownable.transferOwnership(address(0));
|
|
131
|
+
vm.stopPrank();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function testOwnableFollowsProjectOwner(
|
|
135
|
+
address projectOwner,
|
|
136
|
+
address newProjectOwner
|
|
137
|
+
)
|
|
138
|
+
public
|
|
139
|
+
isNotContract(projectOwner)
|
|
140
|
+
isNotContract(newProjectOwner)
|
|
141
|
+
{
|
|
142
|
+
vm.assume(projectOwner != newProjectOwner);
|
|
143
|
+
// `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
|
|
144
|
+
vm.assume(projectOwner != address(0));
|
|
145
|
+
vm.assume(newProjectOwner != address(0));
|
|
146
|
+
|
|
147
|
+
// Create a project for the owner.
|
|
148
|
+
uint256 _projectId = PROJECTS.createFor(projectOwner);
|
|
149
|
+
|
|
150
|
+
// Create the `Ownable` contract.
|
|
151
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
|
|
152
|
+
|
|
153
|
+
// Make sure the project owner owns it.
|
|
154
|
+
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
155
|
+
|
|
156
|
+
// Transfer the project ownership.
|
|
157
|
+
vm.prank(projectOwner);
|
|
158
|
+
PROJECTS.transferFrom(projectOwner, newProjectOwner, _projectId);
|
|
159
|
+
assertEq(PROJECTS.ownerOf(_projectId), newProjectOwner);
|
|
160
|
+
|
|
161
|
+
// Make sure the `Ownable` contract has also been transferred to the new project owner.
|
|
162
|
+
assertEq(newProjectOwner, ownable.owner());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function testOwnableOwnerCanRennounce(address deployer, address owner) public {
|
|
166
|
+
vm.assume(owner != address(0));
|
|
167
|
+
vm.assume(deployer != owner);
|
|
168
|
+
|
|
169
|
+
// Create the `Ownable` contract.
|
|
170
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, owner, uint88(0));
|
|
171
|
+
|
|
172
|
+
// Transfer ownership to the project owner.
|
|
173
|
+
vm.prank(owner);
|
|
174
|
+
ownable.transferOwnership(owner);
|
|
175
|
+
assertEq(owner, ownable.owner(), "Deployer is not the owner.");
|
|
176
|
+
|
|
177
|
+
// Renounce the ownership.
|
|
178
|
+
vm.prank(owner);
|
|
179
|
+
ownable.renounceOwnership();
|
|
180
|
+
assertEq(address(0), ownable.owner(), "Owner was not renounced.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function testJBOwnableOwnerCanRennounce(address deployer, address projectOwner) public isNotContract(projectOwner) {
|
|
184
|
+
// `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
|
|
185
|
+
vm.assume(projectOwner != address(0));
|
|
186
|
+
|
|
187
|
+
// Create a project for the owner.
|
|
188
|
+
uint256 _projectId = PROJECTS.createFor(projectOwner);
|
|
189
|
+
|
|
190
|
+
// Create the `Ownable` contract.
|
|
191
|
+
vm.prank(deployer);
|
|
192
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
|
|
193
|
+
|
|
194
|
+
// Renounce the ownership.
|
|
195
|
+
vm.prank(projectOwner);
|
|
196
|
+
ownable.renounceOwnership();
|
|
197
|
+
assertEq(address(0), ownable.owner(), "Owner was not renounced.");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function testJBOwnablePermissions(
|
|
201
|
+
address projectOwner,
|
|
202
|
+
address callerAddress,
|
|
203
|
+
uint8 requiredPermissionId,
|
|
204
|
+
uint8[] memory permissionIdsToGrant
|
|
205
|
+
)
|
|
206
|
+
public
|
|
207
|
+
isNotContract(projectOwner)
|
|
208
|
+
{
|
|
209
|
+
// `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
|
|
210
|
+
vm.assume(projectOwner != address(0) && callerAddress != projectOwner);
|
|
211
|
+
requiredPermissionId = uint8(bound(uint256(requiredPermissionId), 1, 255));
|
|
212
|
+
|
|
213
|
+
// Truncate array instead of rejecting to avoid exceeding max_test_rejects.
|
|
214
|
+
if (permissionIdsToGrant.length > 4) {
|
|
215
|
+
assembly {
|
|
216
|
+
mstore(permissionIdsToGrant, 4)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Create a project for the owner.
|
|
221
|
+
uint256 _projectId = PROJECTS.createFor(projectOwner);
|
|
222
|
+
|
|
223
|
+
// Create the `Ownable` contract.
|
|
224
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
|
|
225
|
+
|
|
226
|
+
// Set the required permission.
|
|
227
|
+
vm.prank(projectOwner);
|
|
228
|
+
ownable.setPermissionId(requiredPermissionId);
|
|
229
|
+
|
|
230
|
+
// Attempt to call the protected method without permission.
|
|
231
|
+
vm.expectRevert(
|
|
232
|
+
abi.encodeWithSelector(
|
|
233
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
234
|
+
projectOwner,
|
|
235
|
+
callerAddress,
|
|
236
|
+
_projectId,
|
|
237
|
+
requiredPermissionId
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
vm.prank(callerAddress);
|
|
241
|
+
ownable.protectedMethod();
|
|
242
|
+
|
|
243
|
+
// Give permission.
|
|
244
|
+
bool _shouldHavePermission;
|
|
245
|
+
uint8[] memory _permissionIds = new uint8[](permissionIdsToGrant.length);
|
|
246
|
+
for (uint256 i; i < permissionIdsToGrant.length; i++) {
|
|
247
|
+
permissionIdsToGrant[i] = uint8(bound(uint256(permissionIdsToGrant[i]), 1, 255));
|
|
248
|
+
// Check if the permission we need is in the permissions to grant, including if it's ROOT.
|
|
249
|
+
if (permissionIdsToGrant[i] == requiredPermissionId || permissionIdsToGrant[i] == 1) {
|
|
250
|
+
_shouldHavePermission = true;
|
|
251
|
+
}
|
|
252
|
+
_permissionIds[i] = permissionIdsToGrant[i];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The owner gives permission to the caller.
|
|
256
|
+
vm.prank(projectOwner);
|
|
257
|
+
PERMISSIONS.setPermissionsFor(
|
|
258
|
+
projectOwner,
|
|
259
|
+
JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (!_shouldHavePermission) {
|
|
263
|
+
vm.expectRevert(
|
|
264
|
+
abi.encodeWithSelector(
|
|
265
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
266
|
+
projectOwner,
|
|
267
|
+
callerAddress,
|
|
268
|
+
_projectId,
|
|
269
|
+
requiredPermissionId
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
vm.prank(callerAddress);
|
|
275
|
+
ownable.protectedMethod();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function testJBOwnablePermissionsRequiredModifier(
|
|
279
|
+
address projectOwner,
|
|
280
|
+
address callerAddress,
|
|
281
|
+
uint8 requiredPermissionId,
|
|
282
|
+
uint8[] memory permissionIdsToGrant
|
|
283
|
+
)
|
|
284
|
+
public
|
|
285
|
+
isNotContract(projectOwner)
|
|
286
|
+
{
|
|
287
|
+
// `CreateFor` won't work if the address is a contract that doesn't support `ERC721Receiver`.
|
|
288
|
+
vm.assume(projectOwner != address(0) && callerAddress != projectOwner);
|
|
289
|
+
requiredPermissionId = uint8(bound(uint256(requiredPermissionId), 1, 255));
|
|
290
|
+
|
|
291
|
+
// Truncate array instead of rejecting to avoid exceeding max_test_rejects.
|
|
292
|
+
if (permissionIdsToGrant.length > 4) {
|
|
293
|
+
assembly {
|
|
294
|
+
mstore(permissionIdsToGrant, 4)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create a project for the owner.
|
|
299
|
+
uint256 _projectId = PROJECTS.createFor(projectOwner);
|
|
300
|
+
|
|
301
|
+
// Create the `Ownable` contract.
|
|
302
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(_projectId));
|
|
303
|
+
|
|
304
|
+
// Set the permission that is required.
|
|
305
|
+
ownable.setPermission(requiredPermissionId);
|
|
306
|
+
|
|
307
|
+
// Attempt to call the protected method without permission.
|
|
308
|
+
vm.expectRevert(
|
|
309
|
+
abi.encodeWithSelector(
|
|
310
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
311
|
+
projectOwner,
|
|
312
|
+
callerAddress,
|
|
313
|
+
_projectId,
|
|
314
|
+
requiredPermissionId
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
vm.prank(callerAddress);
|
|
318
|
+
ownable.protectedMethodWithRequirePermission();
|
|
319
|
+
|
|
320
|
+
// Give permission.
|
|
321
|
+
bool _shouldHavePermission;
|
|
322
|
+
uint8[] memory _permissionIds = new uint8[](permissionIdsToGrant.length);
|
|
323
|
+
for (uint256 i; i < permissionIdsToGrant.length; i++) {
|
|
324
|
+
permissionIdsToGrant[i] = uint8(bound(uint256(permissionIdsToGrant[i]), 1, 255));
|
|
325
|
+
// Check if the permission we need is in the permissions to grant, including if it's ROOT.
|
|
326
|
+
if (permissionIdsToGrant[i] == requiredPermissionId || permissionIdsToGrant[i] == 1) {
|
|
327
|
+
_shouldHavePermission = true;
|
|
328
|
+
}
|
|
329
|
+
_permissionIds[i] = permissionIdsToGrant[i];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// The owner gives permission to the caller.
|
|
333
|
+
vm.prank(projectOwner);
|
|
334
|
+
PERMISSIONS.setPermissionsFor(
|
|
335
|
+
projectOwner,
|
|
336
|
+
JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (!_shouldHavePermission) {
|
|
340
|
+
vm.expectRevert(
|
|
341
|
+
abi.encodeWithSelector(
|
|
342
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
343
|
+
projectOwner,
|
|
344
|
+
callerAddress,
|
|
345
|
+
_projectId,
|
|
346
|
+
requiredPermissionId
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
vm.prank(callerAddress);
|
|
352
|
+
ownable.protectedMethodWithRequirePermission();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function testCantConfigureOwnerAndProject(address owner, address projectOwner) public isNotContract(projectOwner) {
|
|
356
|
+
vm.assume(owner != address(0) && projectOwner != address(0));
|
|
357
|
+
|
|
358
|
+
// Create a project for the owner.
|
|
359
|
+
uint256 _projectId = PROJECTS.createFor(projectOwner);
|
|
360
|
+
|
|
361
|
+
// Should revert because we set both a owner and a projectOwner
|
|
362
|
+
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
363
|
+
|
|
364
|
+
// Create the `Ownable` contract.
|
|
365
|
+
new MockOwnable(PROJECTS, PERMISSIONS, address(owner), uint88(_projectId));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function testCantInitializeAsRenounced() public {
|
|
369
|
+
// Should revert because we set both a owner and a projectOwner
|
|
370
|
+
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
371
|
+
// Create the `Ownable` contract.
|
|
372
|
+
new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(0));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import "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 {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
13
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
14
|
+
|
|
15
|
+
/// @title OwnableAttacks
|
|
16
|
+
/// @notice Adversarial security tests for JBOwnable covering edge cases
|
|
17
|
+
/// around dual ownership, permission semantics, and renounced contracts.
|
|
18
|
+
contract OwnableAttacks is Test {
|
|
19
|
+
IJBProjects PROJECTS;
|
|
20
|
+
IJBPermissions PERMISSIONS;
|
|
21
|
+
|
|
22
|
+
address alice = makeAddr("alice");
|
|
23
|
+
address bob = makeAddr("bob");
|
|
24
|
+
address attacker = makeAddr("attacker");
|
|
25
|
+
|
|
26
|
+
modifier isNotContract(address a) {
|
|
27
|
+
uint256 size;
|
|
28
|
+
assembly {
|
|
29
|
+
size := extcodesize(a)
|
|
30
|
+
}
|
|
31
|
+
vm.assume(size == 0);
|
|
32
|
+
_;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setUp() public {
|
|
36
|
+
PERMISSIONS = new JBPermissions(address(0));
|
|
37
|
+
PROJECTS = new JBProjects(address(123), address(0), address(0));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =========================================================================
|
|
41
|
+
// Test 1: Constructor rejects both owner AND projectId set
|
|
42
|
+
// =========================================================================
|
|
43
|
+
function test_bothOwnerAndProjectId_constructorReverts() public {
|
|
44
|
+
uint256 projectId = PROJECTS.createFor(alice);
|
|
45
|
+
|
|
46
|
+
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
47
|
+
new MockOwnable(PROJECTS, PERMISSIONS, bob, uint88(projectId));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =========================================================================
|
|
51
|
+
// Test 2: Renounced contract — protectedMethod always reverts
|
|
52
|
+
// =========================================================================
|
|
53
|
+
function test_renounced_protectedMethodAlwaysReverts() public {
|
|
54
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
|
|
55
|
+
|
|
56
|
+
// Owner can call.
|
|
57
|
+
vm.prank(alice);
|
|
58
|
+
ownable.protectedMethod();
|
|
59
|
+
|
|
60
|
+
// Renounce.
|
|
61
|
+
vm.prank(alice);
|
|
62
|
+
ownable.renounceOwnership();
|
|
63
|
+
assertEq(ownable.owner(), address(0), "Should be renounced");
|
|
64
|
+
|
|
65
|
+
// Now NOBODY can call — not alice, not bob, not anyone.
|
|
66
|
+
vm.prank(alice);
|
|
67
|
+
vm.expectRevert();
|
|
68
|
+
ownable.protectedMethod();
|
|
69
|
+
|
|
70
|
+
vm.prank(bob);
|
|
71
|
+
vm.expectRevert();
|
|
72
|
+
ownable.protectedMethod();
|
|
73
|
+
|
|
74
|
+
vm.prank(attacker);
|
|
75
|
+
vm.expectRevert();
|
|
76
|
+
ownable.protectedMethod();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =========================================================================
|
|
80
|
+
// Test 3: Permission ID reset on transfer
|
|
81
|
+
// =========================================================================
|
|
82
|
+
/// @notice After any ownership transfer, permissionId should reset to 0.
|
|
83
|
+
/// This prevents stale permission delegation.
|
|
84
|
+
function test_permissionIdResetOnTransfer() public {
|
|
85
|
+
uint256 projectId = PROJECTS.createFor(alice);
|
|
86
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
|
|
87
|
+
|
|
88
|
+
// Set permission ID.
|
|
89
|
+
vm.prank(alice);
|
|
90
|
+
ownable.setPermissionId(42);
|
|
91
|
+
|
|
92
|
+
(, uint88 pid, uint8 permId) = ownable.jbOwner();
|
|
93
|
+
assertEq(permId, 42, "Permission ID should be 42");
|
|
94
|
+
|
|
95
|
+
// Transfer to bob directly.
|
|
96
|
+
vm.prank(alice);
|
|
97
|
+
ownable.transferOwnership(bob);
|
|
98
|
+
|
|
99
|
+
(, pid, permId) = ownable.jbOwner();
|
|
100
|
+
assertEq(permId, 0, "Permission ID should reset to 0 after transfer");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =========================================================================
|
|
104
|
+
// Test 4: Stale owner after NFT transfer
|
|
105
|
+
// =========================================================================
|
|
106
|
+
/// @notice After transferring project NFT, old owner should lose access.
|
|
107
|
+
function test_staleOwner_afterNFTTransfer() public {
|
|
108
|
+
uint256 projectId = PROJECTS.createFor(alice);
|
|
109
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(projectId));
|
|
110
|
+
|
|
111
|
+
// Alice is current owner.
|
|
112
|
+
assertEq(ownable.owner(), alice);
|
|
113
|
+
vm.prank(alice);
|
|
114
|
+
ownable.protectedMethod(); // Should succeed.
|
|
115
|
+
|
|
116
|
+
// Transfer project NFT to bob.
|
|
117
|
+
vm.prank(alice);
|
|
118
|
+
PROJECTS.transferFrom(alice, bob, projectId);
|
|
119
|
+
|
|
120
|
+
// Alice should no longer be owner.
|
|
121
|
+
assertEq(ownable.owner(), bob, "Bob should be new owner");
|
|
122
|
+
|
|
123
|
+
// Alice cannot call protectedMethod anymore.
|
|
124
|
+
vm.prank(alice);
|
|
125
|
+
vm.expectRevert();
|
|
126
|
+
ownable.protectedMethod();
|
|
127
|
+
|
|
128
|
+
// Bob can call.
|
|
129
|
+
vm.prank(bob);
|
|
130
|
+
ownable.protectedMethod();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =========================================================================
|
|
134
|
+
// Test 5: Transfer to project with overflow ID — must revert
|
|
135
|
+
// =========================================================================
|
|
136
|
+
/// @notice transferOwnershipToProject with projectId > type(uint88).max should revert.
|
|
137
|
+
function test_transferOwnershipToProject_overflowReverts() public {
|
|
138
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, alice, 0);
|
|
139
|
+
|
|
140
|
+
// type(uint88).max + 1 = 309485009821345068724781056
|
|
141
|
+
uint256 overflowId = uint256(type(uint88).max) + 1;
|
|
142
|
+
|
|
143
|
+
vm.prank(alice);
|
|
144
|
+
vm.expectRevert();
|
|
145
|
+
ownable.transferOwnershipToProject(overflowId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// =========================================================================
|
|
149
|
+
// Test 6: ROOT permission on wrong project doesn't grant access
|
|
150
|
+
// =========================================================================
|
|
151
|
+
/// @notice Attacker has ROOT permission on their own project. Verify it
|
|
152
|
+
/// doesn't grant access to a different project's JBOwnable.
|
|
153
|
+
function test_rootOnWrongProject_noAccess() public {
|
|
154
|
+
// Create two projects.
|
|
155
|
+
uint256 aliceProject = PROJECTS.createFor(alice);
|
|
156
|
+
uint256 attackerProject = PROJECTS.createFor(attacker);
|
|
157
|
+
|
|
158
|
+
// Ownable is owned by alice's project.
|
|
159
|
+
MockOwnable ownable = new MockOwnable(PROJECTS, PERMISSIONS, address(0), uint88(aliceProject));
|
|
160
|
+
|
|
161
|
+
// Set permission ID so delegated access is possible.
|
|
162
|
+
vm.prank(alice);
|
|
163
|
+
ownable.setPermissionId(42);
|
|
164
|
+
|
|
165
|
+
// Attacker grants themselves ROOT (permission 1) on their OWN project.
|
|
166
|
+
uint8[] memory rootPerms = new uint8[](1);
|
|
167
|
+
rootPerms[0] = 1; // ROOT
|
|
168
|
+
|
|
169
|
+
vm.prank(attacker);
|
|
170
|
+
PERMISSIONS.setPermissionsFor(
|
|
171
|
+
attacker,
|
|
172
|
+
JBPermissionsData({operator: attacker, projectId: uint56(attackerProject), permissionIds: rootPerms})
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Attacker tries to call protectedMethod — should still fail because
|
|
176
|
+
// ROOT is on attackerProject, not aliceProject.
|
|
177
|
+
vm.prank(attacker);
|
|
178
|
+
vm.expectRevert(
|
|
179
|
+
abi.encodeWithSelector(
|
|
180
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector, alice, attacker, aliceProject, 42
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
ownable.protectedMethod();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {OwnableHandler} from "./handlers/OwnableHandler.sol";
|
|
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
|
+
contract OwnableInvariantTests is Test {
|
|
17
|
+
OwnableHandler handler;
|
|
18
|
+
|
|
19
|
+
function setUp() public {
|
|
20
|
+
handler = new OwnableHandler();
|
|
21
|
+
targetContract(address(handler));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @notice Owner address and project ID are mutually exclusive: can't both be non-zero.
|
|
25
|
+
function invariant_cantBelongToUserAndProject() public {
|
|
26
|
+
(address owner, uint88 projectId,) = handler.OWNABLE().jbOwner();
|
|
27
|
+
assertTrue(owner == address(0) || projectId == uint256(0), "owner and projectId cannot both be non-zero");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// @notice After renouncing, both owner and projectId must be zero.
|
|
31
|
+
function invariant_renounceZerosOut() public {
|
|
32
|
+
if (
|
|
33
|
+
handler.wasEverRenounced()
|
|
34
|
+
&& handler.renounceCount() > handler.transferCount() + handler.projectTransferCount()
|
|
35
|
+
) {
|
|
36
|
+
(address owner, uint88 projectId,) = handler.OWNABLE().jbOwner();
|
|
37
|
+
assertTrue(owner == address(0) && projectId == 0, "renounced state should have zero owner and projectId");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// @notice The permissionId is always reset to 0 on ownership transfers.
|
|
42
|
+
function invariant_permissionIdResetOnTransfer() public {
|
|
43
|
+
(,, uint8 permissionId) = handler.OWNABLE().jbOwner();
|
|
44
|
+
// After any ownership change, permissionId should be 0 (reset by _transferOwnership).
|
|
45
|
+
// This is always true because the handler only calls transfer/renounce functions,
|
|
46
|
+
// and never calls setPermissionId.
|
|
47
|
+
assertEq(permissionId, 0, "permissionId should be 0 after transfers");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// @notice If projectId is set, owner address must be zero.
|
|
51
|
+
function invariant_projectOwnershipExcludesAddress() public {
|
|
52
|
+
(address owner, uint88 projectId,) = handler.OWNABLE().jbOwner();
|
|
53
|
+
if (projectId != 0) {
|
|
54
|
+
assertEq(owner, address(0), "project ownership should zero the owner address");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
// import { Test } from "forge-std/Test.sol";
|
|
5
|
+
import {CommonBase} from "forge-std/Base.sol";
|
|
6
|
+
import {StdCheats} from "forge-std/StdCheats.sol";
|
|
7
|
+
import {StdUtils} from "forge-std/StdUtils.sol";
|
|
8
|
+
import {console} from "forge-std/console.sol";
|
|
9
|
+
|
|
10
|
+
import {MockOwnable, JBOwnableOverrides} from "../mocks/MockOwnable.sol";
|
|
11
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
13
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
14
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
15
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
16
|
+
|
|
17
|
+
contract OwnableHandler is CommonBase, StdCheats, StdUtils {
|
|
18
|
+
IJBProjects public immutable PROJECTS;
|
|
19
|
+
IJBPermissions public immutable PERMISSIONS;
|
|
20
|
+
MockOwnable public immutable OWNABLE;
|
|
21
|
+
|
|
22
|
+
address[] public actors;
|
|
23
|
+
address internal currentActor;
|
|
24
|
+
|
|
25
|
+
// Ghost variables for tracking state.
|
|
26
|
+
uint256 public transferCount;
|
|
27
|
+
uint256 public renounceCount;
|
|
28
|
+
uint256 public projectTransferCount;
|
|
29
|
+
bool public wasEverRenounced;
|
|
30
|
+
|
|
31
|
+
modifier useActor(uint256 actorIndexSeed) {
|
|
32
|
+
currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
|
|
33
|
+
vm.startPrank(currentActor);
|
|
34
|
+
_;
|
|
35
|
+
vm.stopPrank();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
address deployer = vm.addr(1);
|
|
40
|
+
address initialOwner = vm.addr(2);
|
|
41
|
+
// Deploy the permissions contract.
|
|
42
|
+
PERMISSIONS = new JBPermissions(address(0));
|
|
43
|
+
// Deploy the `JBProjects` contract.
|
|
44
|
+
PROJECTS = new JBProjects(address(123), address(0), address(0));
|
|
45
|
+
// Deploy the `JBOwnable` contract.
|
|
46
|
+
vm.prank(deployer);
|
|
47
|
+
OWNABLE = new MockOwnable(PROJECTS, PERMISSIONS, initialOwner, uint88(0));
|
|
48
|
+
|
|
49
|
+
actors.push(deployer);
|
|
50
|
+
actors.push(initialOwner);
|
|
51
|
+
actors.push(address(420));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function transferOwnershipToAddress(uint256 actorIndexSeed, address _newOwner) public useActor(actorIndexSeed) {
|
|
55
|
+
// Skip zero address — that's renounceOwnership's job.
|
|
56
|
+
if (_newOwner == address(0)) return;
|
|
57
|
+
|
|
58
|
+
try OWNABLE.transferOwnership(_newOwner) {
|
|
59
|
+
transferCount++;
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renounceOwnership(uint256 actorIndexSeed) public useActor(actorIndexSeed) {
|
|
64
|
+
try OWNABLE.renounceOwnership() {
|
|
65
|
+
renounceCount++;
|
|
66
|
+
wasEverRenounced = true;
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function transferOwnershipToProject(uint256 actorIndexSeed, uint256 projectId) public useActor(actorIndexSeed) {
|
|
71
|
+
// Bound to valid project ID range (1 to type(uint88).max).
|
|
72
|
+
projectId = bound(projectId, 1, type(uint88).max);
|
|
73
|
+
|
|
74
|
+
try OWNABLE.transferOwnershipToProject(projectId) {
|
|
75
|
+
projectTransferCount++;
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
}
|