@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/foundry.toml +1 -1
- package/package.json +13 -4
- package/src/JBOwnable.sol +5 -10
- package/src/JBOwnableOverrides.sol +29 -23
- package/src/interfaces/IJBOwnable.sol +7 -5
- package/src/structs/JBOwner.sol +6 -6
- package/ADMINISTRATION.md +0 -79
- package/ARCHITECTURE.md +0 -92
- package/AUDIT_INSTRUCTIONS.md +0 -69
- package/RISKS.md +0 -51
- package/SKILLS.md +0 -41
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -117
- package/slither-ci.config.json +0 -10
- package/test/CodexUnmintedProjectHijack.t.sol +0 -45
- package/test/Ownable.t.sol +0 -383
- package/test/OwnableAttacks.t.sol +0 -190
- package/test/OwnableEdgeCases.t.sol +0 -437
- package/test/OwnableInvariantTests.sol +0 -48
- package/test/audit/PermissionDriftAfterProjectTransfer.t.sol +0 -58
- package/test/audit/PermissionIdNFTTransfer.t.sol +0 -93
- package/test/audit/ProjectTransferPermissionPolicy.t.sol +0 -58
- package/test/handlers/OwnableHandler.sol +0 -75
- package/test/regression/BurnLockProtection.t.sol +0 -112
- package/test/regression/RootPermissionBypassesPermissionIdZero.t.sol +0 -87
- package/test/regression/ZeroAddressValidation.t.sol +0 -67
|
@@ -1,190 +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 {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
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
48
|
-
new MockOwnable(projects, permissions, bob, uint88(projectId));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// =========================================================================
|
|
52
|
-
// Test 2: Renounced contract — protectedMethod always reverts
|
|
53
|
-
// =========================================================================
|
|
54
|
-
function test_renounced_protectedMethodAlwaysReverts() public {
|
|
55
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
56
|
-
|
|
57
|
-
// Owner can call.
|
|
58
|
-
vm.prank(alice);
|
|
59
|
-
ownable.protectedMethod();
|
|
60
|
-
|
|
61
|
-
// Renounce.
|
|
62
|
-
vm.prank(alice);
|
|
63
|
-
ownable.renounceOwnership();
|
|
64
|
-
assertEq(ownable.owner(), address(0), "Should be renounced");
|
|
65
|
-
|
|
66
|
-
// Now NOBODY can call — not alice, not bob, not anyone.
|
|
67
|
-
vm.prank(alice);
|
|
68
|
-
vm.expectRevert();
|
|
69
|
-
ownable.protectedMethod();
|
|
70
|
-
|
|
71
|
-
vm.prank(bob);
|
|
72
|
-
vm.expectRevert();
|
|
73
|
-
ownable.protectedMethod();
|
|
74
|
-
|
|
75
|
-
vm.prank(attacker);
|
|
76
|
-
vm.expectRevert();
|
|
77
|
-
ownable.protectedMethod();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// =========================================================================
|
|
81
|
-
// Test 3: Permission ID reset on transfer
|
|
82
|
-
// =========================================================================
|
|
83
|
-
/// @notice After any ownership transfer, permissionId should reset to 0.
|
|
84
|
-
/// This prevents stale permission delegation.
|
|
85
|
-
function test_permissionIdResetOnTransfer() public {
|
|
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));
|
|
89
|
-
|
|
90
|
-
// Set permission ID.
|
|
91
|
-
vm.prank(alice);
|
|
92
|
-
ownable.setPermissionId(42);
|
|
93
|
-
|
|
94
|
-
(, uint88 pid, uint8 permId) = ownable.jbOwner();
|
|
95
|
-
assertEq(permId, 42, "Permission ID should be 42");
|
|
96
|
-
|
|
97
|
-
// Transfer to bob directly.
|
|
98
|
-
vm.prank(alice);
|
|
99
|
-
ownable.transferOwnership(bob);
|
|
100
|
-
|
|
101
|
-
(, pid, permId) = ownable.jbOwner();
|
|
102
|
-
assertEq(permId, 0, "Permission ID should reset to 0 after transfer");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// =========================================================================
|
|
106
|
-
// Test 4: Stale owner after NFT transfer
|
|
107
|
-
// =========================================================================
|
|
108
|
-
/// @notice After transferring project NFT, old owner should lose access.
|
|
109
|
-
function test_staleOwner_afterNFTTransfer() public {
|
|
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));
|
|
113
|
-
|
|
114
|
-
// Alice is current owner.
|
|
115
|
-
assertEq(ownable.owner(), alice);
|
|
116
|
-
vm.prank(alice);
|
|
117
|
-
ownable.protectedMethod(); // Should succeed.
|
|
118
|
-
|
|
119
|
-
// Transfer project NFT to bob.
|
|
120
|
-
vm.prank(alice);
|
|
121
|
-
projects.transferFrom(alice, bob, projectId);
|
|
122
|
-
|
|
123
|
-
// Alice should no longer be owner.
|
|
124
|
-
assertEq(ownable.owner(), bob, "Bob should be new owner");
|
|
125
|
-
|
|
126
|
-
// Alice cannot call protectedMethod anymore.
|
|
127
|
-
vm.prank(alice);
|
|
128
|
-
vm.expectRevert();
|
|
129
|
-
ownable.protectedMethod();
|
|
130
|
-
|
|
131
|
-
// Bob can call.
|
|
132
|
-
vm.prank(bob);
|
|
133
|
-
ownable.protectedMethod();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// =========================================================================
|
|
137
|
-
// Test 5: Transfer to project with overflow ID — must revert
|
|
138
|
-
// =========================================================================
|
|
139
|
-
/// @notice transferOwnershipToProject with projectId > type(uint88).max should revert.
|
|
140
|
-
function test_transferOwnershipToProject_overflowReverts() public {
|
|
141
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
142
|
-
|
|
143
|
-
// type(uint88).max + 1 = 309485009821345068724781056
|
|
144
|
-
uint256 overflowId = uint256(type(uint88).max) + 1;
|
|
145
|
-
|
|
146
|
-
vm.prank(alice);
|
|
147
|
-
vm.expectRevert();
|
|
148
|
-
ownable.transferOwnershipToProject(overflowId);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// =========================================================================
|
|
152
|
-
// Test 6: ROOT permission on wrong project doesn't grant access
|
|
153
|
-
// =========================================================================
|
|
154
|
-
/// @notice Attacker has ROOT permission on their own project. Verify it
|
|
155
|
-
/// doesn't grant access to a different project's JBOwnable.
|
|
156
|
-
function test_rootOnWrongProject_noAccess() public {
|
|
157
|
-
// Create two projects.
|
|
158
|
-
uint256 aliceProject = projects.createFor(alice);
|
|
159
|
-
uint256 attackerProject = projects.createFor(attacker);
|
|
160
|
-
|
|
161
|
-
// Ownable is owned by alice's project.
|
|
162
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
163
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(aliceProject));
|
|
164
|
-
|
|
165
|
-
// Set permission ID so delegated access is possible.
|
|
166
|
-
vm.prank(alice);
|
|
167
|
-
ownable.setPermissionId(42);
|
|
168
|
-
|
|
169
|
-
// Attacker grants themselves ROOT (permission 1) on their OWN project.
|
|
170
|
-
uint8[] memory rootPerms = new uint8[](1);
|
|
171
|
-
rootPerms[0] = 1; // ROOT
|
|
172
|
-
|
|
173
|
-
vm.prank(attacker);
|
|
174
|
-
permissions.setPermissionsFor(
|
|
175
|
-
attacker,
|
|
176
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
177
|
-
JBPermissionsData({operator: attacker, projectId: uint56(attackerProject), permissionIds: rootPerms})
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
// Attacker tries to call protectedMethod — should still fail because
|
|
181
|
-
// ROOT is on attackerProject, not aliceProject.
|
|
182
|
-
vm.prank(attacker);
|
|
183
|
-
vm.expectRevert(
|
|
184
|
-
abi.encodeWithSelector(
|
|
185
|
-
JBPermissioned.JBPermissioned_Unauthorized.selector, alice, attacker, aliceProject, 42
|
|
186
|
-
)
|
|
187
|
-
);
|
|
188
|
-
ownable.protectedMethod();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
@@ -1,437 +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 {MockOwnableERC2771} from "./mocks/MockOwnableERC2771.sol";
|
|
7
|
-
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
8
|
-
import {IJBOwnable} from "../src/interfaces/IJBOwnable.sol";
|
|
9
|
-
|
|
10
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
11
|
-
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
12
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
13
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
14
|
-
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
15
|
-
|
|
16
|
-
/// @title OwnableEdgeCases
|
|
17
|
-
/// @notice Edge case and gap tests for JBOwnable: multi-hop NFT transfers,
|
|
18
|
-
/// project-to-project ownership, permissionId lifecycle, and nonexistent projects.
|
|
19
|
-
contract OwnableEdgeCases is Test {
|
|
20
|
-
IJBProjects projects;
|
|
21
|
-
IJBPermissions permissions;
|
|
22
|
-
|
|
23
|
-
address alice = makeAddr("alice");
|
|
24
|
-
address bob = makeAddr("bob");
|
|
25
|
-
address charlie = makeAddr("charlie");
|
|
26
|
-
address dave = makeAddr("dave");
|
|
27
|
-
|
|
28
|
-
modifier isNotContract(address a) {
|
|
29
|
-
uint256 size;
|
|
30
|
-
assembly {
|
|
31
|
-
size := extcodesize(a)
|
|
32
|
-
}
|
|
33
|
-
vm.assume(size == 0);
|
|
34
|
-
_;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function setUp() public {
|
|
38
|
-
permissions = new JBPermissions(address(0));
|
|
39
|
-
projects = new JBProjects(address(123), address(0), address(0));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// =========================================================================
|
|
43
|
-
// Test 1: Multi-hop NFT transfer — ownership follows through A→B→C→D
|
|
44
|
-
// =========================================================================
|
|
45
|
-
function test_multiHopNFTTransfer_ownerFollows() public {
|
|
46
|
-
uint256 projectId = projects.createFor(alice);
|
|
47
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
48
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
49
|
-
|
|
50
|
-
assertEq(ownable.owner(), alice);
|
|
51
|
-
|
|
52
|
-
// Transfer NFT: alice → bob
|
|
53
|
-
vm.prank(alice);
|
|
54
|
-
projects.transferFrom(alice, bob, projectId);
|
|
55
|
-
assertEq(ownable.owner(), bob, "Should follow to bob");
|
|
56
|
-
|
|
57
|
-
// Transfer NFT: bob → charlie
|
|
58
|
-
vm.prank(bob);
|
|
59
|
-
projects.transferFrom(bob, charlie, projectId);
|
|
60
|
-
assertEq(ownable.owner(), charlie, "Should follow to charlie");
|
|
61
|
-
|
|
62
|
-
// Transfer NFT: charlie → dave
|
|
63
|
-
vm.prank(charlie);
|
|
64
|
-
projects.transferFrom(charlie, dave, projectId);
|
|
65
|
-
assertEq(ownable.owner(), dave, "Should follow to dave");
|
|
66
|
-
|
|
67
|
-
// dave can call protectedMethod, alice/bob/charlie cannot
|
|
68
|
-
vm.prank(dave);
|
|
69
|
-
ownable.protectedMethod();
|
|
70
|
-
|
|
71
|
-
vm.prank(alice);
|
|
72
|
-
vm.expectRevert();
|
|
73
|
-
ownable.protectedMethod();
|
|
74
|
-
|
|
75
|
-
vm.prank(bob);
|
|
76
|
-
vm.expectRevert();
|
|
77
|
-
ownable.protectedMethod();
|
|
78
|
-
|
|
79
|
-
vm.prank(charlie);
|
|
80
|
-
vm.expectRevert();
|
|
81
|
-
ownable.protectedMethod();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// =========================================================================
|
|
85
|
-
// Test 2: Transfer project → different project
|
|
86
|
-
// =========================================================================
|
|
87
|
-
function test_transferProjectToProject() public {
|
|
88
|
-
uint256 projectA = projects.createFor(alice);
|
|
89
|
-
uint256 projectB = projects.createFor(bob);
|
|
90
|
-
|
|
91
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
92
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
|
|
93
|
-
assertEq(ownable.owner(), alice);
|
|
94
|
-
|
|
95
|
-
// Transfer ownership from project A to project B.
|
|
96
|
-
vm.prank(alice);
|
|
97
|
-
ownable.transferOwnershipToProject(projectB);
|
|
98
|
-
|
|
99
|
-
// Owner should now be bob (owner of project B).
|
|
100
|
-
assertEq(ownable.owner(), bob, "Owner should be projectB's owner (bob)");
|
|
101
|
-
|
|
102
|
-
// alice no longer has access
|
|
103
|
-
vm.prank(alice);
|
|
104
|
-
vm.expectRevert();
|
|
105
|
-
ownable.protectedMethod();
|
|
106
|
-
|
|
107
|
-
// bob has access
|
|
108
|
-
vm.prank(bob);
|
|
109
|
-
ownable.protectedMethod();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// =========================================================================
|
|
113
|
-
// Test 3: Full ownership cycle: address → project → address → project
|
|
114
|
-
// =========================================================================
|
|
115
|
-
function test_fullOwnershipCycle() public {
|
|
116
|
-
uint256 projectA = projects.createFor(alice);
|
|
117
|
-
uint256 projectB = projects.createFor(bob);
|
|
118
|
-
|
|
119
|
-
// Start with address ownership.
|
|
120
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, charlie, 0);
|
|
121
|
-
assertEq(ownable.owner(), charlie);
|
|
122
|
-
|
|
123
|
-
// charlie → project A (alice)
|
|
124
|
-
vm.prank(charlie);
|
|
125
|
-
ownable.transferOwnershipToProject(projectA);
|
|
126
|
-
assertEq(ownable.owner(), alice);
|
|
127
|
-
|
|
128
|
-
// project A → bob (address)
|
|
129
|
-
vm.prank(alice);
|
|
130
|
-
ownable.transferOwnership(bob);
|
|
131
|
-
assertEq(ownable.owner(), bob);
|
|
132
|
-
|
|
133
|
-
// bob → project B (bob is also project B owner, but that's fine)
|
|
134
|
-
vm.prank(bob);
|
|
135
|
-
ownable.transferOwnershipToProject(projectB);
|
|
136
|
-
assertEq(ownable.owner(), bob, "bob owns projectB so still bob");
|
|
137
|
-
|
|
138
|
-
// Verify jbOwner struct is correct (projectId set, owner zeroed).
|
|
139
|
-
(address storedOwner, uint88 storedProjectId, uint8 storedPermId) = ownable.jbOwner();
|
|
140
|
-
assertEq(storedOwner, address(0), "owner field should be zero in project mode");
|
|
141
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
142
|
-
assertEq(storedProjectId, uint88(projectB), "projectId should be projectB");
|
|
143
|
-
assertEq(storedPermId, 0, "permissionId should be 0");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// =========================================================================
|
|
147
|
-
// Test 4: permissionId lifecycle through multiple transfers
|
|
148
|
-
// =========================================================================
|
|
149
|
-
function test_permissionIdLifecycle() public {
|
|
150
|
-
uint256 projectA = projects.createFor(alice);
|
|
151
|
-
|
|
152
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
153
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
|
|
154
|
-
|
|
155
|
-
// Set permissionId to 42.
|
|
156
|
-
vm.prank(alice);
|
|
157
|
-
ownable.setPermissionId(42);
|
|
158
|
-
(,, uint8 permId) = ownable.jbOwner();
|
|
159
|
-
assertEq(permId, 42);
|
|
160
|
-
|
|
161
|
-
// Transfer to bob — permissionId should reset.
|
|
162
|
-
vm.prank(alice);
|
|
163
|
-
ownable.transferOwnership(bob);
|
|
164
|
-
(,, permId) = ownable.jbOwner();
|
|
165
|
-
assertEq(permId, 0, "permissionId should reset after transferOwnership");
|
|
166
|
-
|
|
167
|
-
// Set permissionId again as new owner.
|
|
168
|
-
vm.prank(bob);
|
|
169
|
-
ownable.setPermissionId(99);
|
|
170
|
-
(,, permId) = ownable.jbOwner();
|
|
171
|
-
assertEq(permId, 99);
|
|
172
|
-
|
|
173
|
-
// Transfer to project — permissionId should reset again.
|
|
174
|
-
vm.prank(bob);
|
|
175
|
-
ownable.transferOwnershipToProject(projectA);
|
|
176
|
-
(,, permId) = ownable.jbOwner();
|
|
177
|
-
assertEq(permId, 0, "permissionId should reset after transferOwnershipToProject");
|
|
178
|
-
|
|
179
|
-
// Set permissionId as project owner.
|
|
180
|
-
vm.prank(alice);
|
|
181
|
-
ownable.setPermissionId(200);
|
|
182
|
-
(,, permId) = ownable.jbOwner();
|
|
183
|
-
assertEq(permId, 200);
|
|
184
|
-
|
|
185
|
-
// Renounce — permissionId should be 0.
|
|
186
|
-
vm.prank(alice);
|
|
187
|
-
ownable.renounceOwnership();
|
|
188
|
-
(,, permId) = ownable.jbOwner();
|
|
189
|
-
assertEq(permId, 0, "permissionId should be 0 after renounce");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// =========================================================================
|
|
193
|
-
// Test 5: Non-owner cannot call setPermissionId
|
|
194
|
-
// =========================================================================
|
|
195
|
-
function test_nonOwnerCannotSetPermissionId(address nonOwner) public {
|
|
196
|
-
vm.assume(nonOwner != alice && nonOwner != address(0));
|
|
197
|
-
|
|
198
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
199
|
-
|
|
200
|
-
vm.prank(nonOwner);
|
|
201
|
-
vm.expectRevert();
|
|
202
|
-
ownable.setPermissionId(42);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// =========================================================================
|
|
206
|
-
// Test 6: Transfer to nonexistent project reverts
|
|
207
|
-
// =========================================================================
|
|
208
|
-
function test_transferToNonexistentProject_reverts() public {
|
|
209
|
-
// Create one project so count == 1.
|
|
210
|
-
projects.createFor(alice);
|
|
211
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
212
|
-
|
|
213
|
-
// Project 2 doesn't exist.
|
|
214
|
-
vm.prank(alice);
|
|
215
|
-
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ProjectDoesNotExist.selector));
|
|
216
|
-
ownable.transferOwnershipToProject(2);
|
|
217
|
-
|
|
218
|
-
// Project 999 doesn't exist.
|
|
219
|
-
vm.prank(alice);
|
|
220
|
-
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ProjectDoesNotExist.selector));
|
|
221
|
-
ownable.transferOwnershipToProject(999);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// =========================================================================
|
|
225
|
-
// Test 7: Delegated access — permission granted on project, then NFT
|
|
226
|
-
// transferred, old delegate loses access
|
|
227
|
-
// =========================================================================
|
|
228
|
-
function test_delegatedAccess_lostAfterNFTTransfer() public {
|
|
229
|
-
uint256 projectId = projects.createFor(alice);
|
|
230
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
231
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
232
|
-
|
|
233
|
-
// Set permissionId so delegation is possible.
|
|
234
|
-
vm.prank(alice);
|
|
235
|
-
ownable.setPermissionId(42);
|
|
236
|
-
|
|
237
|
-
// Alice grants charlie permission 42 on the project.
|
|
238
|
-
uint8[] memory permIds = new uint8[](1);
|
|
239
|
-
permIds[0] = 42;
|
|
240
|
-
vm.prank(alice);
|
|
241
|
-
permissions.setPermissionsFor(
|
|
242
|
-
alice,
|
|
243
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
244
|
-
JBPermissionsData({operator: charlie, projectId: uint56(projectId), permissionIds: permIds})
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
// Charlie can call protectedMethod (delegated via permissions).
|
|
248
|
-
vm.prank(charlie);
|
|
249
|
-
ownable.protectedMethod();
|
|
250
|
-
|
|
251
|
-
// Transfer NFT to bob.
|
|
252
|
-
vm.prank(alice);
|
|
253
|
-
projects.transferFrom(alice, bob, projectId);
|
|
254
|
-
|
|
255
|
-
// Charlie's delegation was from alice. Now owner is bob.
|
|
256
|
-
// Charlie should lose access because _checkOwner resolves to bob,
|
|
257
|
-
// and charlie has no permissions from bob.
|
|
258
|
-
vm.prank(charlie);
|
|
259
|
-
vm.expectRevert();
|
|
260
|
-
ownable.protectedMethod();
|
|
261
|
-
|
|
262
|
-
// bob can still call directly.
|
|
263
|
-
vm.prank(bob);
|
|
264
|
-
ownable.protectedMethod();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// =========================================================================
|
|
268
|
-
// Test 8: OwnershipTransferred event emitted correctly
|
|
269
|
-
// =========================================================================
|
|
270
|
-
function test_ownershipTransferredEvent() public {
|
|
271
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
272
|
-
|
|
273
|
-
// Transfer to bob — expect event.
|
|
274
|
-
vm.expectEmit(true, true, false, true);
|
|
275
|
-
emit IJBOwnable.OwnershipTransferred(alice, bob, alice);
|
|
276
|
-
|
|
277
|
-
vm.prank(alice);
|
|
278
|
-
ownable.transferOwnership(bob);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// =========================================================================
|
|
282
|
-
// Test 9: PermissionIdChanged event emitted correctly
|
|
283
|
-
// =========================================================================
|
|
284
|
-
function test_permissionIdChangedEvent() public {
|
|
285
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
286
|
-
|
|
287
|
-
vm.expectEmit(true, true, false, true);
|
|
288
|
-
emit IJBOwnable.PermissionIdChanged(42, alice);
|
|
289
|
-
|
|
290
|
-
vm.prank(alice);
|
|
291
|
-
ownable.setPermissionId(42);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// =========================================================================
|
|
295
|
-
// Test 10: Constructor tolerates an unminted project owner
|
|
296
|
-
// =========================================================================
|
|
297
|
-
function test_constructorWithUnmintedProject_emitsZeroOwnerUntilMinted() public {
|
|
298
|
-
vm.expectEmit(true, true, false, true);
|
|
299
|
-
emit IJBOwnable.OwnershipTransferred(address(0), address(0), address(this));
|
|
300
|
-
|
|
301
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(1));
|
|
302
|
-
|
|
303
|
-
assertEq(ownable.owner(), address(0), "Owner should resolve to zero before the project exists");
|
|
304
|
-
|
|
305
|
-
uint256 projectId = projects.createFor(alice);
|
|
306
|
-
assertEq(projectId, 1, "Expected the first minted project to match the configured future owner");
|
|
307
|
-
assertEq(ownable.owner(), alice, "Owner should resolve once the project is minted");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// =========================================================================
|
|
311
|
-
// Test 11: Fuzz — transfer to any valid project, verify owner resolution
|
|
312
|
-
// =========================================================================
|
|
313
|
-
function testFuzz_transferToProject(address projectOwner) public isNotContract(projectOwner) {
|
|
314
|
-
vm.assume(projectOwner != address(0));
|
|
315
|
-
|
|
316
|
-
uint256 projectId = projects.createFor(projectOwner);
|
|
317
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
318
|
-
|
|
319
|
-
vm.prank(alice);
|
|
320
|
-
ownable.transferOwnershipToProject(projectId);
|
|
321
|
-
|
|
322
|
-
assertEq(ownable.owner(), projectOwner, "Owner should match project owner");
|
|
323
|
-
|
|
324
|
-
// Verify jbOwner struct.
|
|
325
|
-
(address storedOwner, uint88 storedProjectId,) = ownable.jbOwner();
|
|
326
|
-
assertEq(storedOwner, address(0), "stored owner should be zero");
|
|
327
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
328
|
-
assertEq(storedProjectId, uint88(projectId), "stored projectId should match");
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// =========================================================================
|
|
332
|
-
// Test 12: Renounced contract cannot reclaim ownership
|
|
333
|
-
// =========================================================================
|
|
334
|
-
/// @notice After renouncing, no one can call transferOwnership, transferOwnershipToProject,
|
|
335
|
-
/// setPermissionId, or renounceOwnership again.
|
|
336
|
-
function test_renouncedContract_cannotReclaim() public {
|
|
337
|
-
uint256 projectId = projects.createFor(alice);
|
|
338
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
339
|
-
|
|
340
|
-
vm.prank(alice);
|
|
341
|
-
ownable.renounceOwnership();
|
|
342
|
-
|
|
343
|
-
// Nobody can transfer ownership back.
|
|
344
|
-
vm.prank(alice);
|
|
345
|
-
vm.expectRevert();
|
|
346
|
-
ownable.transferOwnership(alice);
|
|
347
|
-
|
|
348
|
-
vm.prank(bob);
|
|
349
|
-
vm.expectRevert();
|
|
350
|
-
ownable.transferOwnership(bob);
|
|
351
|
-
|
|
352
|
-
// Nobody can transfer to a project.
|
|
353
|
-
vm.prank(alice);
|
|
354
|
-
vm.expectRevert();
|
|
355
|
-
ownable.transferOwnershipToProject(projectId);
|
|
356
|
-
|
|
357
|
-
// Nobody can set permissionId.
|
|
358
|
-
vm.prank(alice);
|
|
359
|
-
vm.expectRevert();
|
|
360
|
-
ownable.setPermissionId(1);
|
|
361
|
-
|
|
362
|
-
// Nobody can renounce again (already renounced, _checkOwner fails).
|
|
363
|
-
vm.prank(alice);
|
|
364
|
-
vm.expectRevert();
|
|
365
|
-
ownable.renounceOwnership();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// =========================================================================
|
|
369
|
-
// Test 13: _msgSender is NOT ERC2771-aware (design documentation)
|
|
370
|
-
// =========================================================================
|
|
371
|
-
/// @notice JBOwnable uses plain Context._msgSender() (returns msg.sender),
|
|
372
|
-
/// NOT ERC2771Context. This test documents that a trusted forwarder
|
|
373
|
-
/// appending a sender address to calldata does NOT affect _checkOwner.
|
|
374
|
-
function test_noERC2771_trustedForwarderHasNoEffect() public {
|
|
375
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
376
|
-
|
|
377
|
-
// Simulate what a trusted forwarder would do: call with alice's address
|
|
378
|
-
// appended to calldata. Since JBOwnable uses plain Context, this has no effect.
|
|
379
|
-
// The msg.sender is still bob, not alice.
|
|
380
|
-
bytes memory callData = abi.encodeWithSelector(MockOwnable.protectedMethod.selector);
|
|
381
|
-
bytes memory forwardedCallData = abi.encodePacked(callData, alice);
|
|
382
|
-
|
|
383
|
-
vm.prank(bob);
|
|
384
|
-
(bool success,) = address(ownable).call(forwardedCallData);
|
|
385
|
-
assertFalse(success, "Forwarded call should fail - JBOwnable ignores appended sender");
|
|
386
|
-
|
|
387
|
-
// Direct call from alice still works.
|
|
388
|
-
vm.prank(alice);
|
|
389
|
-
ownable.protectedMethod();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// =========================================================================
|
|
393
|
-
// Test 14: OwnershipTransferred event uses _msgSender() (L-27 fix)
|
|
394
|
-
// =========================================================================
|
|
395
|
-
/// @notice When a subclass overrides _msgSender() (e.g., for ERC-2771),
|
|
396
|
-
/// the OwnershipTransferred event's caller field should reflect the
|
|
397
|
-
/// forwarded sender, not msg.sender.
|
|
398
|
-
function test_ownershipTransferredEvent_usesOverriddenMsgSender() public {
|
|
399
|
-
address forwarder = makeAddr("forwarder");
|
|
400
|
-
MockOwnableERC2771 ownable = new MockOwnableERC2771(projects, permissions, alice, 0, forwarder);
|
|
401
|
-
|
|
402
|
-
// Expect event with caller=alice (the forwarded sender), not forwarder.
|
|
403
|
-
vm.expectEmit(true, true, false, true);
|
|
404
|
-
emit IJBOwnable.OwnershipTransferred(alice, bob, alice);
|
|
405
|
-
|
|
406
|
-
// Forwarder calls transferOwnership with alice's address appended (ERC-2771 style).
|
|
407
|
-
bytes memory callData = abi.encodeWithSelector(IJBOwnable.transferOwnership.selector, bob);
|
|
408
|
-
bytes memory forwardedCallData = abi.encodePacked(callData, alice);
|
|
409
|
-
|
|
410
|
-
vm.prank(forwarder);
|
|
411
|
-
(bool success,) = address(ownable).call(forwardedCallData);
|
|
412
|
-
assertTrue(success, "Forwarded transferOwnership should succeed");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// =========================================================================
|
|
416
|
-
// Test 15: PermissionIdChanged event uses _msgSender() (L-27 fix)
|
|
417
|
-
// =========================================================================
|
|
418
|
-
/// @notice When a subclass overrides _msgSender() (e.g., for ERC-2771),
|
|
419
|
-
/// the PermissionIdChanged event's caller field should reflect the
|
|
420
|
-
/// forwarded sender, not msg.sender.
|
|
421
|
-
function test_permissionIdChangedEvent_usesOverriddenMsgSender() public {
|
|
422
|
-
address forwarder = makeAddr("forwarder");
|
|
423
|
-
MockOwnableERC2771 ownable = new MockOwnableERC2771(projects, permissions, alice, 0, forwarder);
|
|
424
|
-
|
|
425
|
-
// Expect event with caller=alice (the forwarded sender), not forwarder.
|
|
426
|
-
vm.expectEmit(true, true, false, true);
|
|
427
|
-
emit IJBOwnable.PermissionIdChanged(42, alice);
|
|
428
|
-
|
|
429
|
-
// Forwarder calls setPermissionId with alice's address appended.
|
|
430
|
-
bytes memory callData = abi.encodeWithSelector(IJBOwnable.setPermissionId.selector, uint8(42));
|
|
431
|
-
bytes memory forwardedCallData = abi.encodePacked(callData, alice);
|
|
432
|
-
|
|
433
|
-
vm.prank(forwarder);
|
|
434
|
-
(bool success,) = address(ownable).call(forwardedCallData);
|
|
435
|
-
assertTrue(success, "Forwarded setPermissionId should succeed");
|
|
436
|
-
}
|
|
437
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
import {OwnableHandler} from "./handlers/OwnableHandler.sol";
|
|
6
|
-
|
|
7
|
-
contract OwnableInvariantTests is Test {
|
|
8
|
-
OwnableHandler handler;
|
|
9
|
-
|
|
10
|
-
function setUp() public {
|
|
11
|
-
handler = new OwnableHandler();
|
|
12
|
-
targetContract(address(handler));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/// @notice Owner address and project ID are mutually exclusive: can't both be non-zero.
|
|
16
|
-
function invariant_cantBelongToUserAndProject() public {
|
|
17
|
-
(address owner, uint88 projectId,) = handler.OWNABLE().jbOwner();
|
|
18
|
-
assertTrue(owner == address(0) || projectId == uint256(0), "owner and projectId cannot both be non-zero");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/// @notice After renouncing, both owner and projectId must be zero.
|
|
22
|
-
function invariant_renounceZerosOut() public {
|
|
23
|
-
if (
|
|
24
|
-
handler.wasEverRenounced()
|
|
25
|
-
&& handler.renounceCount() > handler.transferCount() + handler.projectTransferCount()
|
|
26
|
-
) {
|
|
27
|
-
(address owner, uint88 projectId,) = handler.OWNABLE().jbOwner();
|
|
28
|
-
assertTrue(owner == address(0) && projectId == 0, "renounced state should have zero owner and projectId");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// @notice The permissionId is always reset to 0 on ownership transfers.
|
|
33
|
-
function invariant_permissionIdResetOnTransfer() public {
|
|
34
|
-
(,, uint8 permissionId) = handler.OWNABLE().jbOwner();
|
|
35
|
-
// After any ownership change, permissionId should be 0 (reset by _transferOwnership).
|
|
36
|
-
// This is always true because the handler only calls transfer/renounce functions,
|
|
37
|
-
// and never calls setPermissionId.
|
|
38
|
-
assertEq(permissionId, 0, "permissionId should be 0 after transfers");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/// @notice If projectId is set, owner address must be zero.
|
|
42
|
-
function invariant_projectOwnershipExcludesAddress() public {
|
|
43
|
-
(address owner, uint88 projectId,) = handler.OWNABLE().jbOwner();
|
|
44
|
-
if (projectId != 0) {
|
|
45
|
-
assertEq(owner, address(0), "project ownership should zero the owner address");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|