@bananapus/ownable-v6 0.0.20 → 0.0.22
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/ownable-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"node": ">=20.0.0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@bananapus/core-v6": "^0.0.
|
|
14
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
13
|
+
"@bananapus/core-v6": "^0.0.36",
|
|
14
|
+
"@bananapus/permission-ids-v6": "^0.0.19",
|
|
15
15
|
"@openzeppelin/contracts": "^5.6.1"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
@@ -36,6 +36,14 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
36
36
|
/// @notice This contract's owner information.
|
|
37
37
|
JBOwner public override jbOwner;
|
|
38
38
|
|
|
39
|
+
//*********************************************************************//
|
|
40
|
+
// -------------------- internal stored properties ------------------- //
|
|
41
|
+
//*********************************************************************//
|
|
42
|
+
|
|
43
|
+
/// @notice The resolved owner address at the time permissionId was last set.
|
|
44
|
+
/// @dev Used to detect stale permissions after ownership changes (e.g., NFT transfer).
|
|
45
|
+
address internal _permissionOwner;
|
|
46
|
+
|
|
39
47
|
//*********************************************************************//
|
|
40
48
|
// -------------------------- constructor ---------------------------- //
|
|
41
49
|
//*********************************************************************//
|
|
@@ -134,9 +142,16 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
144
|
|
|
145
|
+
// Detect stale permissions: if ownership changed since permissionId was set
|
|
146
|
+
// (e.g., project NFT transferred), treat permissionId as 0 (direct-owner-only).
|
|
147
|
+
uint8 effectivePermissionId = ownerInfo.permissionId;
|
|
148
|
+
if (effectivePermissionId != 0 && resolvedOwner != _permissionOwner) {
|
|
149
|
+
effectivePermissionId = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
// When permissionId is 0 (direct-owner-only mode), bypass the permission system entirely.
|
|
138
153
|
// This ensures ROOT operators cannot act as owner when delegation is disabled.
|
|
139
|
-
if (
|
|
154
|
+
if (effectivePermissionId == 0) {
|
|
140
155
|
if (_msgSender() != resolvedOwner) {
|
|
141
156
|
revert JBPermissioned.JBPermissioned_Unauthorized({
|
|
142
157
|
account: resolvedOwner, sender: _msgSender(), projectId: ownerInfo.projectId, permissionId: 0
|
|
@@ -146,7 +161,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
_requirePermissionFrom({
|
|
149
|
-
account: resolvedOwner, projectId: ownerInfo.projectId, permissionId:
|
|
164
|
+
account: resolvedOwner, projectId: ownerInfo.projectId, permissionId: effectivePermissionId
|
|
150
165
|
});
|
|
151
166
|
}
|
|
152
167
|
|
|
@@ -221,6 +236,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
221
236
|
/// @param permissionId The permission ID to use for `onlyOwner`.
|
|
222
237
|
function _setPermissionId(uint8 permissionId) internal virtual {
|
|
223
238
|
jbOwner.permissionId = permissionId;
|
|
239
|
+
_permissionOwner = owner();
|
|
224
240
|
emit PermissionIdChanged({newId: permissionId, caller: _msgSender()});
|
|
225
241
|
}
|
|
226
242
|
|
|
@@ -257,6 +273,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
257
273
|
// Update the stored owner information to the new owner and reset the `permissionId`.
|
|
258
274
|
// This is to prevent permissions clashes for the new user/owner.
|
|
259
275
|
jbOwner = JBOwner({owner: newOwner, projectId: projectId, permissionId: 0});
|
|
276
|
+
_permissionOwner = address(0);
|
|
260
277
|
// Emit a transfer event with the new owner's address.
|
|
261
278
|
_emitTransferEvent({previousOwner: oldOwner, newOwner: newOwner, newProjectId: projectId});
|
|
262
279
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
7
|
+
|
|
8
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
9
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
10
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
11
|
+
|
|
12
|
+
contract PermissionDriftAfterProjectTransferTest is Test {
|
|
13
|
+
JBPermissions internal permissions;
|
|
14
|
+
JBProjects internal projects;
|
|
15
|
+
|
|
16
|
+
address internal alice = makeAddr("alice");
|
|
17
|
+
address internal bob = makeAddr("bob");
|
|
18
|
+
address internal operator = makeAddr("operator");
|
|
19
|
+
|
|
20
|
+
function setUp() public {
|
|
21
|
+
permissions = new JBPermissions(address(0));
|
|
22
|
+
projects = new JBProjects(address(this), address(0), address(0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function test_operatorCannotInheritOwnerAccessAfterProjectNftTransfer() public {
|
|
26
|
+
uint256 projectId = projects.createFor(alice);
|
|
27
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
28
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
29
|
+
|
|
30
|
+
vm.prank(alice);
|
|
31
|
+
ownable.setPermissionId(42);
|
|
32
|
+
|
|
33
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
34
|
+
permissionIds[0] = 42;
|
|
35
|
+
|
|
36
|
+
vm.prank(bob);
|
|
37
|
+
permissions.setPermissionsFor(
|
|
38
|
+
bob, JBPermissionsData({operator: operator, projectId: 0, permissionIds: permissionIds})
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
vm.prank(alice);
|
|
42
|
+
projects.transferFrom(alice, bob, projectId);
|
|
43
|
+
|
|
44
|
+
assertEq(ownable.owner(), bob, "project NFT transfer makes bob the direct owner");
|
|
45
|
+
(, uint88 storedProjectId, uint8 storedPermissionId) = ownable.jbOwner();
|
|
46
|
+
assertEq(storedProjectId, projectId, "ownable remains project-owned");
|
|
47
|
+
assertEq(storedPermissionId, 42, "old owner's permissionId survived the ownership change in storage");
|
|
48
|
+
|
|
49
|
+
// After the fix, the stale permissionId is ignored because the resolved owner
|
|
50
|
+
// (bob) differs from _permissionOwner (alice). The operator cannot seize ownership.
|
|
51
|
+
vm.prank(operator);
|
|
52
|
+
vm.expectRevert();
|
|
53
|
+
ownable.transferOwnership(operator);
|
|
54
|
+
|
|
55
|
+
// Bob is still the owner.
|
|
56
|
+
assertEq(ownable.owner(), bob, "bob retains ownership; operator was blocked");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
7
|
+
|
|
8
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
9
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
10
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
11
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
13
|
+
|
|
14
|
+
contract PermissionIdNFTTransferTest is Test {
|
|
15
|
+
IJBProjects internal projects;
|
|
16
|
+
IJBPermissions internal permissions;
|
|
17
|
+
|
|
18
|
+
address internal seller = makeAddr("seller");
|
|
19
|
+
address internal buyer = makeAddr("buyer");
|
|
20
|
+
address internal buyerOperator = makeAddr("buyerOperator");
|
|
21
|
+
|
|
22
|
+
function setUp() public {
|
|
23
|
+
permissions = new JBPermissions(address(0));
|
|
24
|
+
projects = new JBProjects(address(this), address(0), address(0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function test_projectNftTransferInvalidatesStalePermissionId() external {
|
|
28
|
+
uint256 projectId = projects.createFor(seller);
|
|
29
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
30
|
+
|
|
31
|
+
// Seller sets permissionId 30 while they own the project.
|
|
32
|
+
vm.prank(seller);
|
|
33
|
+
ownable.setPermissionId(30);
|
|
34
|
+
|
|
35
|
+
// Buyer grants their own operator permissionId 30 for this project.
|
|
36
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
37
|
+
permissionIds[0] = 30;
|
|
38
|
+
vm.prank(buyer);
|
|
39
|
+
permissions.setPermissionsFor(
|
|
40
|
+
buyer,
|
|
41
|
+
JBPermissionsData({operator: buyerOperator, projectId: uint56(projectId), permissionIds: permissionIds})
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Seller transfers the project NFT to buyer.
|
|
45
|
+
vm.prank(seller);
|
|
46
|
+
projects.transferFrom(seller, buyer, projectId);
|
|
47
|
+
|
|
48
|
+
assertEq(ownable.owner(), buyer, "project NFT transfer changes the effective owner");
|
|
49
|
+
|
|
50
|
+
// The stored permissionId is still 30 in the struct, but the fix makes it
|
|
51
|
+
// stale because the owner changed since the permissionId was set.
|
|
52
|
+
(,, uint8 inheritedPermissionId) = ownable.jbOwner();
|
|
53
|
+
assertEq(inheritedPermissionId, 30, "struct still holds old permissionId");
|
|
54
|
+
|
|
55
|
+
// The stale permissionId should NOT allow the buyer's operator through.
|
|
56
|
+
// After the fix, _checkOwner treats the permissionId as 0 (direct-owner-only)
|
|
57
|
+
// because the resolved owner != _permissionOwner.
|
|
58
|
+
vm.prank(buyerOperator);
|
|
59
|
+
vm.expectRevert();
|
|
60
|
+
ownable.protectedMethod();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function test_newOwnerCanSetOwnPermissionIdAndDelegateAccess() external {
|
|
64
|
+
uint256 projectId = projects.createFor(seller);
|
|
65
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
66
|
+
|
|
67
|
+
// Seller sets permissionId 30 while they own the project.
|
|
68
|
+
vm.prank(seller);
|
|
69
|
+
ownable.setPermissionId(30);
|
|
70
|
+
|
|
71
|
+
// Transfer the project NFT to buyer.
|
|
72
|
+
vm.prank(seller);
|
|
73
|
+
projects.transferFrom(seller, buyer, projectId);
|
|
74
|
+
|
|
75
|
+
// Buyer grants their own operator permissionId 30 for this project.
|
|
76
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
77
|
+
permissionIds[0] = 30;
|
|
78
|
+
vm.prank(buyer);
|
|
79
|
+
permissions.setPermissionsFor(
|
|
80
|
+
buyer,
|
|
81
|
+
JBPermissionsData({operator: buyerOperator, projectId: uint56(projectId), permissionIds: permissionIds})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Buyer explicitly sets the permissionId on the ownable contract.
|
|
85
|
+
// This updates _permissionOwner to the buyer, making the permissionId valid.
|
|
86
|
+
vm.prank(buyer);
|
|
87
|
+
ownable.setPermissionId(30);
|
|
88
|
+
|
|
89
|
+
// Now buyerOperator should be able to call the protected method.
|
|
90
|
+
vm.prank(buyerOperator);
|
|
91
|
+
ownable.protectedMethod();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
7
|
+
|
|
8
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
9
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
10
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
11
|
+
|
|
12
|
+
contract ProjectTransferPermissionPolicyTest is Test {
|
|
13
|
+
JBPermissions internal permissions;
|
|
14
|
+
JBProjects internal projects;
|
|
15
|
+
|
|
16
|
+
address internal seller = makeAddr("seller");
|
|
17
|
+
address internal buyer = makeAddr("buyer");
|
|
18
|
+
address internal operator = makeAddr("operator");
|
|
19
|
+
|
|
20
|
+
function setUp() public {
|
|
21
|
+
permissions = new JBPermissions(address(0));
|
|
22
|
+
projects = new JBProjects(address(this), address(0), address(0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function test_stalePermissionIdAfterProjectTransferBlocksBuyerOperator() public {
|
|
26
|
+
uint256 projectId = projects.createFor(seller);
|
|
27
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
28
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
29
|
+
|
|
30
|
+
// The seller chooses a non-root ecosystem permission as this ownable's owner-equivalent permission.
|
|
31
|
+
vm.prank(seller);
|
|
32
|
+
ownable.setPermissionId(30);
|
|
33
|
+
|
|
34
|
+
// The buyer has independently delegated permission 30 across their projects for unrelated operations.
|
|
35
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
36
|
+
permissionIds[0] = 30;
|
|
37
|
+
|
|
38
|
+
vm.prank(buyer);
|
|
39
|
+
permissions.setPermissionsFor(
|
|
40
|
+
buyer, JBPermissionsData({operator: operator, projectId: 0, permissionIds: permissionIds})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Buying/transferring the project NFT changes the effective owner, but does not reset permissionId in storage.
|
|
44
|
+
vm.prank(seller);
|
|
45
|
+
projects.transferFrom(seller, buyer, projectId);
|
|
46
|
+
|
|
47
|
+
assertEq(ownable.owner(), buyer, "buyer is now the effective owner");
|
|
48
|
+
|
|
49
|
+
(,, uint8 inheritedPermissionId) = ownable.jbOwner();
|
|
50
|
+
assertEq(inheritedPermissionId, 30, "seller-selected permission policy persists in storage");
|
|
51
|
+
|
|
52
|
+
// After the fix, the stale permissionId is ignored because the resolved owner (buyer)
|
|
53
|
+
// differs from _permissionOwner (seller). The operator is blocked.
|
|
54
|
+
vm.prank(operator);
|
|
55
|
+
vm.expectRevert();
|
|
56
|
+
ownable.protectedMethod();
|
|
57
|
+
}
|
|
58
|
+
}
|