@bananapus/ownable-v6 0.0.21 → 0.0.23
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/JBOwnableOverrides.sol +19 -2
- 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/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,75 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
-
pragma solidity 0.8.28;
|
|
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 {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
9
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
10
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
11
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
|
-
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
13
|
-
|
|
14
|
-
contract OwnableHandler is CommonBase, StdCheats, StdUtils {
|
|
15
|
-
IJBProjects public immutable PROJECTS;
|
|
16
|
-
IJBPermissions public immutable PERMISSIONS;
|
|
17
|
-
MockOwnable public immutable OWNABLE;
|
|
18
|
-
|
|
19
|
-
address[] public actors;
|
|
20
|
-
address internal currentActor;
|
|
21
|
-
|
|
22
|
-
// Ghost variables for tracking state.
|
|
23
|
-
uint256 public transferCount;
|
|
24
|
-
uint256 public renounceCount;
|
|
25
|
-
uint256 public projectTransferCount;
|
|
26
|
-
bool public wasEverRenounced;
|
|
27
|
-
|
|
28
|
-
modifier useActor(uint256 actorIndexSeed) {
|
|
29
|
-
currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
|
|
30
|
-
vm.startPrank(currentActor);
|
|
31
|
-
_;
|
|
32
|
-
vm.stopPrank();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
constructor() {
|
|
36
|
-
address deployer = vm.addr(1);
|
|
37
|
-
address initialOwner = vm.addr(2);
|
|
38
|
-
// Deploy the permissions contract.
|
|
39
|
-
PERMISSIONS = new JBPermissions(address(0));
|
|
40
|
-
// Deploy the `JBProjects` contract.
|
|
41
|
-
PROJECTS = new JBProjects(address(123), address(0), address(0));
|
|
42
|
-
// Deploy the `JBOwnable` contract.
|
|
43
|
-
vm.prank(deployer);
|
|
44
|
-
OWNABLE = new MockOwnable(PROJECTS, PERMISSIONS, initialOwner, uint88(0));
|
|
45
|
-
|
|
46
|
-
actors.push(deployer);
|
|
47
|
-
actors.push(initialOwner);
|
|
48
|
-
actors.push(address(420));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function transferOwnershipToAddress(uint256 actorIndexSeed, address _newOwner) public useActor(actorIndexSeed) {
|
|
52
|
-
// Skip zero address — that's renounceOwnership's job.
|
|
53
|
-
if (_newOwner == address(0)) return;
|
|
54
|
-
|
|
55
|
-
try OWNABLE.transferOwnership(_newOwner) {
|
|
56
|
-
transferCount++;
|
|
57
|
-
} catch {}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function renounceOwnership(uint256 actorIndexSeed) public useActor(actorIndexSeed) {
|
|
61
|
-
try OWNABLE.renounceOwnership() {
|
|
62
|
-
renounceCount++;
|
|
63
|
-
wasEverRenounced = true;
|
|
64
|
-
} catch {}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function transferOwnershipToProject(uint256 actorIndexSeed, uint256 projectId) public useActor(actorIndexSeed) {
|
|
68
|
-
// Bound to valid project ID range (1 to type(uint88).max).
|
|
69
|
-
projectId = bound(projectId, 1, type(uint88).max);
|
|
70
|
-
|
|
71
|
-
try OWNABLE.transferOwnershipToProject(projectId) {
|
|
72
|
-
projectTransferCount++;
|
|
73
|
-
} catch {}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
@@ -1,112 +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
|
-
|
|
7
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
8
|
-
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
9
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
10
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
11
|
-
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
12
|
-
|
|
13
|
-
/// @title BurnLockProtection
|
|
14
|
-
/// @notice Verifies that if a project NFT is burned/invalidated,
|
|
15
|
-
/// owner() returns address(0) and _checkOwner() reverts gracefully instead of
|
|
16
|
-
/// permanently locking the contract with an unrecoverable revert.
|
|
17
|
-
contract BurnLockProtection is Test {
|
|
18
|
-
IJBProjects projects;
|
|
19
|
-
IJBPermissions permissions;
|
|
20
|
-
|
|
21
|
-
address alice = makeAddr("alice");
|
|
22
|
-
address bob = makeAddr("bob");
|
|
23
|
-
|
|
24
|
-
function setUp() public {
|
|
25
|
-
permissions = new JBPermissions(address(0));
|
|
26
|
-
projects = new JBProjects(address(123), address(0), address(0));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/// @notice When a project NFT is burned (simulated via mockCallRevert), owner() should
|
|
30
|
-
/// return address(0) instead of reverting — contract degrades to "renounced" state.
|
|
31
|
-
function test_burnedProjectNFT_ownerReturnsZero() public {
|
|
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
|
-
|
|
36
|
-
// Verify normal operation first.
|
|
37
|
-
assertEq(ownable.owner(), alice, "Owner should be alice before burn");
|
|
38
|
-
|
|
39
|
-
// Simulate project NFT burn by making ownerOf revert for this projectId.
|
|
40
|
-
vm.mockCallRevert(
|
|
41
|
-
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// After burn, owner() should return address(0) — NOT revert.
|
|
45
|
-
address resolvedOwner = ownable.owner();
|
|
46
|
-
assertEq(resolvedOwner, address(0), "owner() should return address(0) when project NFT is burned");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/// @notice When a project NFT is burned, _checkOwner() should revert with the standard
|
|
50
|
-
/// Unauthorized error (not an unrecoverable ownerOf revert), making the contract
|
|
51
|
-
/// behave as if ownership was renounced.
|
|
52
|
-
function test_burnedProjectNFT_checkOwnerRevertsGracefully() public {
|
|
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));
|
|
56
|
-
|
|
57
|
-
// Alice can call the protected method before burn.
|
|
58
|
-
vm.prank(alice);
|
|
59
|
-
ownable.protectedMethod();
|
|
60
|
-
|
|
61
|
-
// Simulate project NFT burn.
|
|
62
|
-
vm.mockCallRevert(
|
|
63
|
-
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
// After burn, nobody can call protected methods — but the revert is graceful
|
|
67
|
-
// (Unauthorized from _requirePermissionFrom, not a raw ownerOf revert).
|
|
68
|
-
vm.prank(alice);
|
|
69
|
-
vm.expectRevert();
|
|
70
|
-
ownable.protectedMethod();
|
|
71
|
-
|
|
72
|
-
vm.prank(bob);
|
|
73
|
-
vm.expectRevert();
|
|
74
|
-
ownable.protectedMethod();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/// @notice Address-based ownership is unaffected by the try-catch change.
|
|
78
|
-
function test_addressBasedOwnership_unaffectedByTryCatch() public {
|
|
79
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
80
|
-
|
|
81
|
-
assertEq(ownable.owner(), alice, "Owner should be alice");
|
|
82
|
-
|
|
83
|
-
vm.prank(alice);
|
|
84
|
-
ownable.protectedMethod();
|
|
85
|
-
|
|
86
|
-
// Transfer to bob.
|
|
87
|
-
vm.prank(alice);
|
|
88
|
-
ownable.transferOwnership(bob);
|
|
89
|
-
assertEq(ownable.owner(), bob, "Owner should be bob after transfer");
|
|
90
|
-
|
|
91
|
-
vm.prank(bob);
|
|
92
|
-
ownable.protectedMethod();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/// @notice Normal project-based ownership still works correctly after the fix.
|
|
96
|
-
function test_normalProjectOwnership_stillWorks() public {
|
|
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));
|
|
100
|
-
|
|
101
|
-
assertEq(ownable.owner(), alice);
|
|
102
|
-
|
|
103
|
-
// Transfer project NFT.
|
|
104
|
-
vm.prank(alice);
|
|
105
|
-
projects.transferFrom(alice, bob, projectId);
|
|
106
|
-
|
|
107
|
-
assertEq(ownable.owner(), bob, "Owner should follow project NFT transfer");
|
|
108
|
-
|
|
109
|
-
vm.prank(bob);
|
|
110
|
-
ownable.protectedMethod();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
|
|
6
|
-
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
7
|
-
|
|
8
|
-
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
9
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
10
|
-
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
11
|
-
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
12
|
-
|
|
13
|
-
contract RootPermissionBypassesPermissionIdZeroTest is Test {
|
|
14
|
-
JBProjects internal projects;
|
|
15
|
-
JBPermissions internal permissions;
|
|
16
|
-
|
|
17
|
-
address internal alice = makeAddr("alice");
|
|
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
|
-
/// @notice After M-39 fix: ROOT operator is rejected when permissionId=0 (direct-owner-only mode).
|
|
26
|
-
function test_rootPermissionRejectedWhenPermissionIdIsZero() public {
|
|
27
|
-
uint256 projectId = projects.createFor(alice);
|
|
28
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
29
|
-
|
|
30
|
-
// Grant ROOT permission (id=1) to operator.
|
|
31
|
-
uint8[] memory permissionIds = new uint8[](1);
|
|
32
|
-
permissionIds[0] = 1;
|
|
33
|
-
|
|
34
|
-
vm.prank(alice);
|
|
35
|
-
permissions.setPermissionsFor(
|
|
36
|
-
alice, JBPermissionsData({operator: operator, projectId: uint56(projectId), permissionIds: permissionIds})
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
(, uint88 storedProjectId, uint8 permissionId) = ownable.jbOwner();
|
|
40
|
-
assertEq(storedProjectId, projectId);
|
|
41
|
-
assertEq(permissionId, 0, "expected direct-owner-only mode");
|
|
42
|
-
|
|
43
|
-
// Operator should be rejected when permissionId=0.
|
|
44
|
-
vm.prank(operator);
|
|
45
|
-
vm.expectRevert(
|
|
46
|
-
abi.encodeWithSelector(JBPermissioned.JBPermissioned_Unauthorized.selector, alice, operator, projectId, 0)
|
|
47
|
-
);
|
|
48
|
-
ownable.protectedMethod();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/// @notice Direct owner still works when permissionId=0.
|
|
52
|
-
function test_directOwnerStillWorksWithPermissionIdZero() public {
|
|
53
|
-
uint256 projectId = projects.createFor(alice);
|
|
54
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
55
|
-
|
|
56
|
-
(, uint88 storedProjectId, uint8 permissionId) = ownable.jbOwner();
|
|
57
|
-
assertEq(storedProjectId, projectId);
|
|
58
|
-
assertEq(permissionId, 0);
|
|
59
|
-
|
|
60
|
-
// Alice (project owner) should still be able to call the protected method.
|
|
61
|
-
vm.prank(alice);
|
|
62
|
-
ownable.protectedMethod();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/// @notice Non-zero permissionId still delegates correctly via the permission system.
|
|
66
|
-
function test_delegatedOperatorWorksWhenPermissionIdNonZero() public {
|
|
67
|
-
uint256 projectId = projects.createFor(alice);
|
|
68
|
-
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
69
|
-
|
|
70
|
-
// Set permissionId to 42 (non-zero = delegation enabled).
|
|
71
|
-
vm.prank(alice);
|
|
72
|
-
ownable.setPermissionId(42);
|
|
73
|
-
|
|
74
|
-
// Grant permission 42 to operator.
|
|
75
|
-
uint8[] memory permissionIds = new uint8[](1);
|
|
76
|
-
permissionIds[0] = 42;
|
|
77
|
-
|
|
78
|
-
vm.prank(alice);
|
|
79
|
-
permissions.setPermissionsFor(
|
|
80
|
-
alice, JBPermissionsData({operator: operator, projectId: uint56(projectId), permissionIds: permissionIds})
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// Operator should succeed with matching permissionId.
|
|
84
|
-
vm.prank(operator);
|
|
85
|
-
ownable.protectedMethod();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1,67 +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 {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
10
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
11
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
|
-
|
|
13
|
-
/// @title ZeroAddressValidation
|
|
14
|
-
/// @notice Verifies that deploying with a zero-address projects
|
|
15
|
-
/// contract and a non-zero projectId reverts at construction time, preventing
|
|
16
|
-
/// permanently broken project-based ownership.
|
|
17
|
-
contract ZeroAddressValidation is Test {
|
|
18
|
-
IJBProjects projects;
|
|
19
|
-
IJBPermissions permissions;
|
|
20
|
-
|
|
21
|
-
address alice = makeAddr("alice");
|
|
22
|
-
|
|
23
|
-
function setUp() public {
|
|
24
|
-
permissions = new JBPermissions(address(0));
|
|
25
|
-
projects = new JBProjects(address(123), address(0), address(0));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/// @notice Deploying with projects=address(0) and non-zero projectId must revert.
|
|
29
|
-
function test_zeroProjectsWithProjectId_reverts() public {
|
|
30
|
-
vm.expectRevert(
|
|
31
|
-
abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
|
|
32
|
-
);
|
|
33
|
-
new MockOwnable(IJBProjects(address(0)), permissions, address(0), uint88(1));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// @notice Fuzz: any non-zero projectId with projects=address(0) must revert.
|
|
37
|
-
function testFuzz_zeroProjectsWithAnyProjectId_reverts(uint88 projectId) public {
|
|
38
|
-
vm.assume(projectId != 0);
|
|
39
|
-
|
|
40
|
-
vm.expectRevert(
|
|
41
|
-
abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
|
|
42
|
-
);
|
|
43
|
-
new MockOwnable(IJBProjects(address(0)), permissions, address(0), projectId);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/// @notice Deploying with projects=address(0) and projectId=0 (address-based ownership)
|
|
47
|
-
/// should NOT revert for this error — it's valid as long as initialOwner != address(0).
|
|
48
|
-
function test_zeroProjectsWithAddressOwnership_succeeds() public {
|
|
49
|
-
// This is valid: address-based ownership with projects=address(0).
|
|
50
|
-
MockOwnable ownable = new MockOwnable(IJBProjects(address(0)), permissions, alice, uint88(0));
|
|
51
|
-
assertEq(ownable.owner(), alice, "Owner should be alice with address-based ownership");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// @notice Normal deployment with valid projects contract and projectId succeeds.
|
|
55
|
-
function test_validProjectsWithProjectId_succeeds() public {
|
|
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));
|
|
59
|
-
assertEq(ownable.owner(), alice, "Owner should be alice via project NFT");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/// @notice The existing check for both zero owner and zero projectId is still enforced.
|
|
63
|
-
function test_bothZero_stillReverts() public {
|
|
64
|
-
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
65
|
-
new MockOwnable(projects, permissions, address(0), uint88(0));
|
|
66
|
-
}
|
|
67
|
-
}
|