@bananapus/ownable-v6 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +55 -71
- package/ARCHITECTURE.md +75 -48
- package/AUDIT_INSTRUCTIONS.md +46 -165
- package/CHANGELOG.md +39 -0
- package/README.md +72 -92
- package/RISKS.md +42 -18
- package/SKILLS.md +28 -172
- package/STYLE_GUIDE.md +58 -20
- package/USER_JOURNEYS.md +81 -162
- package/foundry.toml +2 -0
- package/package.json +3 -3
- package/references/operations.md +14 -0
- package/references/runtime.md +19 -0
- package/src/JBOwnableOverrides.sol +4 -0
- package/src/structs/JBOwner.sol +0 -1
- package/test/CodexUnmintedProjectHijack.t.sol +45 -0
- package/CHANGE_LOG.md +0 -235
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,13 +21,13 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity
|
|
24
|
+
pragma solidity 0.8.28;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
|
28
28
|
|
|
29
|
-
// Libraries —
|
|
30
|
-
pragma solidity
|
|
29
|
+
// Libraries — pin to exact version like contracts
|
|
30
|
+
pragma solidity 0.8.28;
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Imports
|
|
@@ -86,12 +86,20 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
86
86
|
|
|
87
87
|
uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
88
88
|
|
|
89
|
+
//*********************************************************************//
|
|
90
|
+
// ------------------------ private constants ------------------------ //
|
|
91
|
+
//*********************************************************************//
|
|
92
|
+
|
|
89
93
|
//*********************************************************************//
|
|
90
94
|
// --------------- public immutable stored properties ---------------- //
|
|
91
95
|
//*********************************************************************//
|
|
92
96
|
|
|
93
97
|
IJBDirectory public immutable override DIRECTORY;
|
|
94
98
|
|
|
99
|
+
//*********************************************************************//
|
|
100
|
+
// -------------- internal immutable stored properties -------------- //
|
|
101
|
+
//*********************************************************************//
|
|
102
|
+
|
|
95
103
|
//*********************************************************************//
|
|
96
104
|
// --------------------- public stored properties -------------------- //
|
|
97
105
|
//*********************************************************************//
|
|
@@ -100,10 +108,26 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
100
108
|
// -------------------- internal stored properties ------------------- //
|
|
101
109
|
//*********************************************************************//
|
|
102
110
|
|
|
111
|
+
//*********************************************************************//
|
|
112
|
+
// -------------------- private stored properties -------------------- //
|
|
113
|
+
//*********************************************************************//
|
|
114
|
+
|
|
115
|
+
//*********************************************************************//
|
|
116
|
+
// ------------------- transient stored properties ------------------- //
|
|
117
|
+
//*********************************************************************//
|
|
118
|
+
|
|
103
119
|
//*********************************************************************//
|
|
104
120
|
// -------------------------- constructor ---------------------------- //
|
|
105
121
|
//*********************************************************************//
|
|
106
122
|
|
|
123
|
+
//*********************************************************************//
|
|
124
|
+
// ---------------------------- modifiers ---------------------------- //
|
|
125
|
+
//*********************************************************************//
|
|
126
|
+
|
|
127
|
+
//*********************************************************************//
|
|
128
|
+
// ------------------------- receive / fallback ---------------------- //
|
|
129
|
+
//*********************************************************************//
|
|
130
|
+
|
|
107
131
|
//*********************************************************************//
|
|
108
132
|
// ---------------------- external transactions ---------------------- //
|
|
109
133
|
//*********************************************************************//
|
|
@@ -112,10 +136,18 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
112
136
|
// ----------------------- external views ---------------------------- //
|
|
113
137
|
//*********************************************************************//
|
|
114
138
|
|
|
139
|
+
//*********************************************************************//
|
|
140
|
+
// -------------------------- public views --------------------------- //
|
|
141
|
+
//*********************************************************************//
|
|
142
|
+
|
|
115
143
|
//*********************************************************************//
|
|
116
144
|
// ----------------------- public transactions ----------------------- //
|
|
117
145
|
//*********************************************************************//
|
|
118
146
|
|
|
147
|
+
//*********************************************************************//
|
|
148
|
+
// ---------------------- internal transactions ---------------------- //
|
|
149
|
+
//*********************************************************************//
|
|
150
|
+
|
|
119
151
|
//*********************************************************************//
|
|
120
152
|
// ----------------------- internal helpers -------------------------- //
|
|
121
153
|
//*********************************************************************//
|
|
@@ -134,17 +166,28 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
134
166
|
1. Custom errors
|
|
135
167
|
2. Public constants
|
|
136
168
|
3. Internal constants
|
|
137
|
-
4.
|
|
138
|
-
5.
|
|
139
|
-
6.
|
|
140
|
-
7.
|
|
141
|
-
8.
|
|
142
|
-
9.
|
|
143
|
-
10.
|
|
144
|
-
11.
|
|
145
|
-
12.
|
|
146
|
-
13.
|
|
147
|
-
14.
|
|
169
|
+
4. Private constants
|
|
170
|
+
5. Public immutable stored properties
|
|
171
|
+
6. Internal immutable stored properties
|
|
172
|
+
7. Public stored properties
|
|
173
|
+
8. Internal stored properties
|
|
174
|
+
9. Private stored properties
|
|
175
|
+
10. Transient stored properties
|
|
176
|
+
11. Constructor
|
|
177
|
+
12. Modifiers
|
|
178
|
+
13. Receive / fallback
|
|
179
|
+
14. External transactions
|
|
180
|
+
15. External views
|
|
181
|
+
16. Public views
|
|
182
|
+
17. Public transactions
|
|
183
|
+
18. Internal transactions
|
|
184
|
+
19. Internal helpers
|
|
185
|
+
20. Internal views
|
|
186
|
+
21. Private helpers
|
|
187
|
+
|
|
188
|
+
Use these additional section labels where they better match the contents of the block:
|
|
189
|
+
- `internal functions` is accepted as equivalent to `internal helpers`
|
|
190
|
+
- `events` and `structs` are acceptable in specialized contracts that define them explicitly
|
|
148
191
|
|
|
149
192
|
Functions are alphabetized within each section.
|
|
150
193
|
|
|
@@ -326,7 +369,7 @@ Standard config across all repos:
|
|
|
326
369
|
|
|
327
370
|
```toml
|
|
328
371
|
[profile.default]
|
|
329
|
-
solc = '0.8.
|
|
372
|
+
solc = '0.8.28'
|
|
330
373
|
evm_version = 'cancun'
|
|
331
374
|
optimizer_runs = 200
|
|
332
375
|
libs = ["node_modules", "lib"]
|
|
@@ -565,8 +608,3 @@ CI checks formatting via `forge fmt --check`.
|
|
|
565
608
|
### Contract Size Checks
|
|
566
609
|
|
|
567
610
|
CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
## Repo-Specific Deviations
|
|
571
|
-
|
|
572
|
-
None. This repo follows the standard configuration exactly.
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -1,200 +1,119 @@
|
|
|
1
|
-
# User Journeys
|
|
1
|
+
# User Journeys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Repo Purpose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This repo adapts `Ownable`-style control to Juicebox project ownership and project-scoped operator permissions.
|
|
6
|
+
It is an ownership adapter. It does not replace the underlying ownership or permission registries in
|
|
7
|
+
[nana-core-v6](../nana-core-v6/USER_JOURNEYS.md).
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Primary Actors
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
- protocol or product teams that want `onlyOwner` to follow a project NFT
|
|
12
|
+
- operators who need owner-like access without receiving the project itself
|
|
13
|
+
- auditors checking whether delegated owner semantics strand or over-grant authority
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
## Key Surfaces
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
16
|
-
- `initialOwner` -- Set to `address(0)` because ownership is project-based
|
|
17
|
-
- `initialProjectIdOwner` -- The ID of the Juicebox project whose NFT holder becomes the owner
|
|
17
|
+
- `JBOwnable`: `Ownable`-style adapter whose owner follows a Juicebox project
|
|
18
|
+
- `JBOwnableOverrides`: extension that lets a project-scoped permission satisfy `onlyOwner`
|
|
19
|
+
- `owner()`, `transferOwnership(...)`, `transferOwnershipToProject(...)`, `setPermissionId(...)`: core ownership-resolution and migration paths
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
1. `PROJECTS` immutable set to `projects`
|
|
21
|
-
2. `PERMISSIONS` immutable set to `permissions` (inherited from `JBPermissioned`)
|
|
22
|
-
3. Constructor validates that if `initialProjectIdOwner != 0`, then `address(projects) != address(0)` (reverts with `JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner` if the project ID is non-zero but the projects contract is the zero address). There is no project existence check in the constructor.
|
|
23
|
-
4. Constructor validates that at least one of `initialOwner` or `initialProjectIdOwner` is non-zero (reverts with `JBOwnableOverrides_InvalidNewOwner` if both are zero)
|
|
24
|
-
5. `_transferOwnership(address(0), projectId)` executes:
|
|
25
|
-
- Sets `jbOwner = JBOwner({owner: address(0), projectId: projectId, permissionId: 0})`
|
|
26
|
-
- Calls `_emitTransferEvent(address(0), address(0), projectId)`
|
|
27
|
-
6. `owner()` now resolves dynamically via `PROJECTS.ownerOf(projectId)`
|
|
21
|
+
## Journey 1: Give A Contract To A Juicebox Project Instead Of A Wallet
|
|
28
22
|
|
|
29
|
-
**
|
|
23
|
+
**Actor:** downstream contract author.
|
|
30
24
|
|
|
31
|
-
**
|
|
32
|
-
- If the project does not exist (ID > `PROJECTS.count()`), the constructor still succeeds -- the existence check is only enforced in `transferOwnershipToProject`, not the constructor. If the project NFT has not yet been minted, `PROJECTS.ownerOf()` reverts, and the try-catch in `owner()` returns `address(0)`, effectively locking the contract until the project is minted.
|
|
33
|
-
- `owner()` returns the current NFT holder dynamically. If the NFT is transferred, ownership automatically follows -- no on-chain update to the JBOwnable contract is needed.
|
|
34
|
-
- Deploying with both `initialOwner == address(0)` and `initialProjectIdOwner == 0` reverts with `JBOwnableOverrides_InvalidNewOwner`. To create an unowned contract, set an owner and call `renounceOwnership()` in the constructor body.
|
|
25
|
+
**Intent:** make a contract follow Juicebox project ownership instead of a fixed EOA or multisig.
|
|
35
26
|
|
|
36
|
-
**
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- `owner()` returns the current NFT holder, not a cached value
|
|
27
|
+
**Preconditions**
|
|
28
|
+
- the downstream contract wants `onlyOwner` ergonomics
|
|
29
|
+
- a project ID and `JBProjects` dependency are already known
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
**Main Flow**
|
|
32
|
+
1. Inherit `JBOwnable` or `JBOwnableOverrides`.
|
|
33
|
+
2. Initialize ownership with the relevant project ID and `JBProjects` reference.
|
|
34
|
+
3. Let `owner()` resolve through the current project NFT holder rather than a fixed address.
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
**Failure Modes**
|
|
37
|
+
- the contract assumes ordinary `Ownable` transfer semantics after adopting project-based ownership
|
|
38
|
+
- the wrong project ID is configured
|
|
39
|
+
- reviewers ignore the adapter and audit the downstream contract as if `owner` were fixed
|
|
44
40
|
|
|
45
|
-
**
|
|
41
|
+
**Postconditions**
|
|
42
|
+
- `owner()` now resolves through the configured project NFT instead of a fixed wallet
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
## Journey 2: Delegate Owner-Level Access To Operators
|
|
48
45
|
|
|
49
|
-
**
|
|
50
|
-
- `permissions` -- The `IJBPermissions` contract used for delegated access checks
|
|
51
|
-
- `projects` -- The `IJBProjects` contract (can be `address(0)` when not using project-based ownership)
|
|
52
|
-
- `initialOwner` -- The address that becomes the contract owner (must not be `address(0)`)
|
|
53
|
-
- `initialProjectIdOwner` -- Set to `0` because ownership is address-based
|
|
46
|
+
**Actor:** current project owner.
|
|
54
47
|
|
|
55
|
-
**
|
|
56
|
-
1. `PROJECTS` immutable set to `projects`
|
|
57
|
-
2. `PERMISSIONS` immutable set to `permissions` (inherited from `JBPermissioned`)
|
|
58
|
-
3. Constructor validates that `initialOwner != address(0)` (reverts with `JBOwnableOverrides_InvalidNewOwner` if both `initialOwner` and `initialProjectIdOwner` are zero)
|
|
59
|
-
4. `_transferOwnership(initialOwner, 0)` executes:
|
|
60
|
-
- Sets `jbOwner = JBOwner({owner: initialOwner, projectId: 0, permissionId: 0})`
|
|
61
|
-
- Calls `_emitTransferEvent(address(0), initialOwner, 0)`
|
|
48
|
+
**Intent:** let an operator satisfy `onlyOwner` for one contract without transferring the project.
|
|
62
49
|
|
|
63
|
-
**
|
|
50
|
+
**Preconditions**
|
|
51
|
+
- the downstream contract uses `JBOwnableOverrides`
|
|
52
|
+
- the team has chosen the permission ID that should count as delegated owner access
|
|
64
53
|
|
|
65
|
-
**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
**Main Flow**
|
|
55
|
+
1. Choose the permission ID the downstream contract should respect.
|
|
56
|
+
2. Grant that permission through `JBPermissions`.
|
|
57
|
+
3. `JBOwnableOverrides` treats the operator as satisfying `onlyOwner` for that contract.
|
|
69
58
|
|
|
70
|
-
|
|
59
|
+
**Failure Modes**
|
|
60
|
+
- teams grant a broader permission than intended
|
|
61
|
+
- downstream reviewers forget that `onlyOwner` may resolve through permissions instead of direct ownership
|
|
62
|
+
- operators retain stale permissions after governance changes
|
|
71
63
|
|
|
72
|
-
|
|
64
|
+
**Postconditions**
|
|
65
|
+
- the chosen operator can satisfy `onlyOwner` without receiving direct ownership of the project or contract
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
## Journey 3: Change The Delegated Permission ID Without Changing Ownership
|
|
75
68
|
|
|
76
|
-
**
|
|
69
|
+
**Actor:** current effective owner.
|
|
77
70
|
|
|
78
|
-
**
|
|
79
|
-
- `newOwner` -- The address to transfer ownership to (must not be `address(0)`)
|
|
71
|
+
**Intent:** rotate delegated owner policy without changing the underlying owner.
|
|
80
72
|
|
|
81
|
-
**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
3. `_transferOwnership(newOwner, 0)` executes:
|
|
85
|
-
- Records `oldOwner` (resolved from current `jbOwner`, with try-catch for burned project NFTs)
|
|
86
|
-
- Overwrites `jbOwner = JBOwner({owner: newOwner, projectId: 0, permissionId: 0})`
|
|
87
|
-
- Calls `_emitTransferEvent(oldOwner, newOwner, 0)`
|
|
73
|
+
**Preconditions**
|
|
74
|
+
- the contract already uses `JBOwnableOverrides`
|
|
75
|
+
- all operators who need continued access can be regranted under the new permission ID
|
|
88
76
|
|
|
89
|
-
**
|
|
77
|
+
**Main Flow**
|
|
78
|
+
1. Update the permission ID the adapter treats as owner-equivalent with `setPermissionId(...)`.
|
|
79
|
+
2. Re-grant the new permission where needed.
|
|
80
|
+
3. Re-audit operator assumptions because the old permission no longer satisfies `onlyOwner`.
|
|
90
81
|
|
|
91
|
-
**
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
- The previous owner (or their delegates) can no longer call `onlyOwner` functions
|
|
95
|
-
- `newOwner` can immediately call `onlyOwner` functions without any additional setup
|
|
82
|
+
**Failure Modes**
|
|
83
|
+
- operator access disappears unintentionally after a permission-ID rotation
|
|
84
|
+
- teams forget that old delegations stop working immediately
|
|
96
85
|
|
|
97
|
-
|
|
86
|
+
**Postconditions**
|
|
87
|
+
- the adapter now resolves delegated owner access through the new permission ID only
|
|
98
88
|
|
|
99
|
-
## Journey
|
|
89
|
+
## Journey 4: Transfer Or Burn Ownership Deliberately
|
|
100
90
|
|
|
101
|
-
**
|
|
91
|
+
**Actor:** current effective owner.
|
|
102
92
|
|
|
103
|
-
**
|
|
93
|
+
**Intent:** move or remove control with full awareness of the consequences.
|
|
104
94
|
|
|
105
|
-
**
|
|
106
|
-
-
|
|
95
|
+
**Preconditions**
|
|
96
|
+
- the team understands whether admin recovery should remain possible
|
|
97
|
+
- downstream integrations can tolerate the new owner model
|
|
107
98
|
|
|
108
|
-
**
|
|
109
|
-
1. `
|
|
110
|
-
2.
|
|
111
|
-
3.
|
|
112
|
-
4. `_transferOwnership(address(0), uint88(projectId))` executes:
|
|
113
|
-
- Records `oldOwner` (resolved from current `jbOwner`)
|
|
114
|
-
- Overwrites `jbOwner = JBOwner({owner: address(0), projectId: uint88(projectId), permissionId: 0})`
|
|
115
|
-
- Calls `_emitTransferEvent(oldOwner, address(0), uint88(projectId))`
|
|
99
|
+
**Main Flow**
|
|
100
|
+
1. Use `transferOwnership(...)` for an address owner or `transferOwnershipToProject(...)` for a project owner.
|
|
101
|
+
2. Re-establish delegated permission policy if the new owner still wants operators.
|
|
102
|
+
3. Renounce or burn only when permanent admin loss is intentional.
|
|
116
103
|
|
|
117
|
-
**
|
|
104
|
+
**Failure Modes**
|
|
105
|
+
- ownership is burned even though the downstream contract still needs administration
|
|
106
|
+
- teams forget that permission-ID delegation resets across ownership changes
|
|
118
107
|
|
|
119
|
-
**
|
|
120
|
-
-
|
|
121
|
-
- The `uint88` cast does not truncate (the preceding `type(uint88).max` check ensures this)
|
|
122
|
-
- `permissionId` is reset to 0 on transfer. The new project owner must call `setPermissionId()` to configure delegation.
|
|
123
|
-
- If the project NFT is subsequently burned (hypothetically), `owner()` returns `address(0)` and the contract is effectively renounced
|
|
124
|
-
- Unlike the constructor, `_emitTransferEvent` calls `PROJECTS.ownerOf(newProjectId)` without try-catch -- if the project does not exist, the transaction reverts. This is intentional: the `PROJECTS.count()` check above prevents this path.
|
|
108
|
+
**Postconditions**
|
|
109
|
+
- control moves to the chosen address or project, or is intentionally removed
|
|
125
110
|
|
|
126
|
-
|
|
111
|
+
## Trust Boundaries
|
|
127
112
|
|
|
128
|
-
|
|
113
|
+
- this repo trusts `JBProjects` for project ownership and `JBPermissions` for delegated authority
|
|
114
|
+
- downstream contracts still need their own audit because this adapter changes who satisfies `onlyOwner`
|
|
129
115
|
|
|
130
|
-
|
|
116
|
+
## Hand-Offs
|
|
131
117
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
**Parameters**:
|
|
135
|
-
- `permissionId` -- The new permission ID to use for `onlyOwner` access delegation
|
|
136
|
-
|
|
137
|
-
**State changes**:
|
|
138
|
-
1. `_checkOwner()` validates the caller
|
|
139
|
-
2. `_setPermissionId(permissionId)` writes `jbOwner.permissionId = permissionId`
|
|
140
|
-
|
|
141
|
-
**Events**: `PermissionIdChanged(uint8 newId, address caller)` -- emitted as `PermissionIdChanged(permissionId, msg.sender)`
|
|
142
|
-
|
|
143
|
-
**Granting the permission to operators** (external step, not on JBOwnable):
|
|
144
|
-
- The owner calls `permissions.setPermissionsFor(account, JBPermissionsData({operator: operatorAddress, projectId: projectId, permissionIds: [permissionId]}))` on the `JBPermissions` contract
|
|
145
|
-
- Operators can then call any `onlyOwner` function. `_checkOwner()` resolves the owner and calls `_requirePermissionFrom(resolvedOwner, projectId, permissionId)`, which passes if the operator has the matching permission.
|
|
146
|
-
|
|
147
|
-
**Edge cases**:
|
|
148
|
-
- `permissionId == 0` effectively disables delegation (permission ID 0 cannot be set in `JBPermissions`). Only the owner (or ROOT holders) can call `onlyOwner` functions.
|
|
149
|
-
- If the owner transfers ownership, `permissionId` resets to 0. The new owner must re-configure delegation.
|
|
150
|
-
- ROOT (permission ID 1) bypasses the configured `permissionId` check, but only when the operator has been granted ROOT **by the resolved owner** via `JBPermissions`. ROOT is not a global override -- it is scoped to the `account` (i.e. the resolved owner) that granted it. This is a feature of `JBPermissioned`, not specific to `JBOwnable`. After renouncement (resolved owner = `address(0)`), ROOT cannot help because no one can obtain permissions granted by `address(0)`.
|
|
151
|
-
- The operator's access is not stored on the JBOwnable contract -- it lives in JBPermissions. Changing the `permissionId` on JBOwnable instantly changes which JBPermissions grants are recognized.
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## Journey 5: Renounce Ownership
|
|
156
|
-
|
|
157
|
-
**Entry point**: `JBOwnableOverrides.renounceOwnership()`
|
|
158
|
-
|
|
159
|
-
**Who can call**: The current owner (resolved via `PROJECTS.ownerOf()` if project-owned, or `jbOwner.owner` if address-owned), or any address with the configured `permissionId` (or ROOT) via `JBPermissions`.
|
|
160
|
-
|
|
161
|
-
**Parameters**: None.
|
|
162
|
-
|
|
163
|
-
**State changes**:
|
|
164
|
-
1. `_checkOwner()` validates the caller
|
|
165
|
-
2. `_transferOwnership(address(0), 0)` executes:
|
|
166
|
-
- Records `oldOwner` (resolved from current `jbOwner`)
|
|
167
|
-
- Overwrites `jbOwner = JBOwner({owner: address(0), projectId: 0, permissionId: 0})`
|
|
168
|
-
- Calls `_emitTransferEvent(oldOwner, address(0), 0)`
|
|
169
|
-
|
|
170
|
-
**Events**: `OwnershipTransferred(address indexed previousOwner, address indexed newOwner, address caller)` -- emitted as `OwnershipTransferred(oldOwner, address(0), msg.sender)`
|
|
171
|
-
|
|
172
|
-
**Edge cases**:
|
|
173
|
-
- After renouncing, `transferOwnership`, `transferOwnershipToProject`, `setPermissionId`, and `renounceOwnership` all revert
|
|
174
|
-
- There is no recovery mechanism. No admin backdoor. No timelock. Renouncement is permanent.
|
|
175
|
-
- A second call to `renounceOwnership()` also reverts (because `_checkOwner()` fails)
|
|
176
|
-
- Even ROOT holders cannot act as owner after renouncement. Although `_requirePermissionFrom(address(0), 0, 0)` is called with `includeRoot: true`, no operator can possess ROOT (or any permission) granted by `address(0)` -- `JBPermissions.setPermissionsFor` requires the caller to be the account or an existing ROOT operator of the account, and no one starts with permissions from `address(0)`
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Journey 6: Implicit Renouncement via Project NFT Burn
|
|
181
|
-
|
|
182
|
-
**Actor**: None (system behavior).
|
|
183
|
-
|
|
184
|
-
**Who can call**: N/A -- this is an emergent behavior, not a direct function call.
|
|
185
|
-
|
|
186
|
-
**Parameters**: None.
|
|
187
|
-
|
|
188
|
-
**Precondition**: The contract is project-owned (`jbOwner.projectId != 0`). The project NFT is burned or otherwise invalidated (note: JBProjects V6 has no burn function, so this is a defensive scenario).
|
|
189
|
-
|
|
190
|
-
**State changes**:
|
|
191
|
-
1. `PROJECTS.ownerOf(projectId)` starts reverting (ERC-721 `ownerOf` reverts for burned tokens)
|
|
192
|
-
2. `owner()` catches the revert via try-catch and returns `address(0)`
|
|
193
|
-
3. `_checkOwner()` catches the revert and resolves owner to `address(0)`, causing `_requirePermissionFrom(address(0), projectId, permissionId)` to fail for any `msg.sender` -- no operator can possess permissions granted by `address(0)` (same reason as explicit renouncement in Journey 5)
|
|
194
|
-
|
|
195
|
-
**Events**: None (no transaction occurs on the JBOwnable contract).
|
|
196
|
-
|
|
197
|
-
**Edge cases**:
|
|
198
|
-
- There is no way to "revive" ownership after the NFT is burned. However, re-minting an NFT with the same ID (if possible) would restore ownership.
|
|
199
|
-
- The `jbOwner` struct is NOT cleared -- it still shows the old `projectId`. Only the resolved owner is `address(0)`.
|
|
200
|
-
- This behavior is consistent between `owner()` and `_checkOwner()` (both use the same try-catch pattern)
|
|
118
|
+
- Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for the project-NFT and permission machinery this adapter depends on.
|
|
119
|
+
- Use [nana-permission-ids-v6](../nana-permission-ids-v6/USER_JOURNEYS.md) if you need the shared numeric permission vocabulary for delegated `onlyOwner` checks.
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/ownable-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
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.34",
|
|
14
|
+
"@bananapus/permission-ids-v6": "^0.0.17",
|
|
15
15
|
"@openzeppelin/contracts": "^5.6.1"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Ownable Operations
|
|
2
|
+
|
|
3
|
+
## Change Checklist
|
|
4
|
+
|
|
5
|
+
- If you edit owner resolution, verify both direct ownership and project-owned cases.
|
|
6
|
+
- If you edit permission handling, verify transfer-time reset behavior.
|
|
7
|
+
- If an integration expects long-lived delegated access, confirm whether the transfer-reset rule invalidates that assumption.
|
|
8
|
+
- If the change touches project ownership, check unminted-project and burn-lock regressions before assuming the happy-path tests are enough.
|
|
9
|
+
|
|
10
|
+
## Common Failure Modes
|
|
11
|
+
|
|
12
|
+
- Integrations assume delegated operators survive ownership transfer.
|
|
13
|
+
- Bugs are blamed on this repo when the underlying project NFT ownership changed upstream.
|
|
14
|
+
- A project-owned contract is treated like an address-owned contract and the wrong actor is allowed through `onlyOwner`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Ownable Runtime
|
|
2
|
+
|
|
3
|
+
## Core Roles
|
|
4
|
+
|
|
5
|
+
- [`src/JBOwnable.sol`](../src/JBOwnable.sol) is the concrete downstream inheritance surface.
|
|
6
|
+
- [`src/JBOwnableOverrides.sol`](../src/JBOwnableOverrides.sol) owns owner resolution and delegated permission checks.
|
|
7
|
+
|
|
8
|
+
## High-Risk Areas
|
|
9
|
+
|
|
10
|
+
- Effective-owner resolution: ownership may follow a project NFT rather than a fixed address.
|
|
11
|
+
- Delegated `onlyOwner` permissions: the chosen permission ID changes who can administer a contract.
|
|
12
|
+
- Transfer semantics: permission IDs reset on transfer, which is safer but easy to forget.
|
|
13
|
+
|
|
14
|
+
## Tests To Trust First
|
|
15
|
+
|
|
16
|
+
- [`test/Ownable.t.sol`](../test/Ownable.t.sol) for baseline behavior.
|
|
17
|
+
- [`test/OwnableEdgeCases.t.sol`](../test/OwnableEdgeCases.t.sol) and [`test/OwnableAttacks.t.sol`](../test/OwnableAttacks.t.sol) for edge and adversarial cases.
|
|
18
|
+
- [`test/OwnableInvariantTests.sol`](../test/OwnableInvariantTests.sol) for broader invariants.
|
|
19
|
+
- [`test/regression/BurnLockProtection.t.sol`](../test/regression/BurnLockProtection.t.sol), [`test/regression/ZeroAddressValidation.t.sol`](../test/regression/ZeroAddressValidation.t.sol), and [`test/CodexUnmintedProjectHijack.t.sol`](../test/CodexUnmintedProjectHijack.t.sol) for the regressions most likely to matter in review.
|
|
@@ -45,6 +45,10 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
45
45
|
/// To restrict access to a specific address, pass that address as the `initialOwner` and `0` as the
|
|
46
46
|
/// `initialProjectIdOwner`.
|
|
47
47
|
/// @dev The owner can give owner access to other addresses through the `permissions` contract.
|
|
48
|
+
/// @dev If `initialProjectIdOwner` references a project ID that has not yet been minted, all ownership checks will
|
|
49
|
+
/// revert until that project is created, leaving the contract unusable. Deployers must ensure that the referenced
|
|
50
|
+
/// project is minted before or atomically with this contract's deployment — this is a deployment trust
|
|
51
|
+
/// assumption.
|
|
48
52
|
/// @param permissions A contract storing permissions.
|
|
49
53
|
/// @param projects Mints ERC-721s that represent project ownership and transfers.
|
|
50
54
|
/// @param initialOwner The owner if the `initialProjectIdOwner` is 0 (until ownership is transferred).
|
package/src/structs/JBOwner.sol
CHANGED
|
@@ -7,7 +7,6 @@ pragma solidity ^0.8.0;
|
|
|
7
7
|
/// `owner` address has owner access.
|
|
8
8
|
/// @custom:member permissionId The permission ID which corresponds to owner access. See `JBPermissions` in `nana-core`
|
|
9
9
|
/// and `nana-permission-ids`.
|
|
10
|
-
// forge-lint: disable-next-line(pascal-case-struct)
|
|
11
10
|
struct JBOwner {
|
|
12
11
|
address owner;
|
|
13
12
|
uint88 projectId;
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
13
|
+
contract CodexUnmintedProjectHijackTest is Test {
|
|
14
|
+
IJBProjects internal projects;
|
|
15
|
+
IJBPermissions internal permissions;
|
|
16
|
+
|
|
17
|
+
address internal deployer = makeAddr("deployer");
|
|
18
|
+
address internal attacker = makeAddr("attacker");
|
|
19
|
+
address internal intendedOwner = makeAddr("intendedOwner");
|
|
20
|
+
|
|
21
|
+
function setUp() public {
|
|
22
|
+
permissions = new JBPermissions(address(0));
|
|
23
|
+
projects = new JBProjects(address(this), address(0), address(0));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function test_unmintedProjectOwnerCanBeHijackedByFirstMinter() external {
|
|
27
|
+
vm.prank(deployer);
|
|
28
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), 1);
|
|
29
|
+
|
|
30
|
+
assertEq(ownable.owner(), address(0), "owner should be unresolved before project 1 exists");
|
|
31
|
+
|
|
32
|
+
vm.prank(attacker);
|
|
33
|
+
uint256 hijackedProjectId = projects.createFor(attacker);
|
|
34
|
+
|
|
35
|
+
assertEq(hijackedProjectId, 1, "attacker should receive the configured project ID");
|
|
36
|
+
assertEq(ownable.owner(), attacker, "first minter becomes owner of the ownable contract");
|
|
37
|
+
|
|
38
|
+
vm.prank(attacker);
|
|
39
|
+
ownable.protectedMethod();
|
|
40
|
+
|
|
41
|
+
vm.prank(intendedOwner);
|
|
42
|
+
vm.expectRevert();
|
|
43
|
+
ownable.protectedMethod();
|
|
44
|
+
}
|
|
45
|
+
}
|