@bananapus/ownable-v6 0.0.8 → 0.0.10
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 +1 -1
- package/AUDIT_INSTRUCTIONS.md +133 -0
- package/CHANGE_LOG.md +221 -0
- package/RISKS.md +15 -17
- package/STYLE_GUIDE.md +16 -2
- package/USER_JOURNEYS.md +245 -0
- package/package.json +3 -3
- package/src/JBOwnable.sol +4 -1
- package/src/JBOwnableOverrides.sol +12 -7
- package/src/structs/JBOwner.sol +1 -0
- package/test/Ownable.t.sol +39 -30
- package/test/OwnableAttacks.t.sol +23 -18
- package/test/OwnableEdgeCases.t.sol +41 -35
- package/test/OwnableInvariantTests.sol +0 -9
- package/test/handlers/OwnableHandler.sol +1 -4
- package/test/mocks/MockOwnable.sol +1 -1
- package/test/regression/{L65_BurnLockProtection.t.sol → BurnLockProtection.t.sol} +19 -17
- package/test/regression/{L66_ZeroAddressValidation.t.sol → ZeroAddressValidation.t.sol} +15 -14
package/USER_JOURNEYS.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# User Journeys -- nana-ownable-v6
|
|
2
|
+
|
|
3
|
+
Concrete end-to-end flows through the JBOwnable system. Each journey traces the exact function calls, state changes, and external interactions.
|
|
4
|
+
|
|
5
|
+
## Journey 1: Deploy a Project-Owned Contract
|
|
6
|
+
|
|
7
|
+
**Actor:** Protocol developer deploying a hook or extension that should be owned by a Juicebox project.
|
|
8
|
+
**Goal:** Create a contract where the project NFT holder has owner access.
|
|
9
|
+
|
|
10
|
+
### Precondition
|
|
11
|
+
|
|
12
|
+
A Juicebox project exists with ID `projectId`. The `JBProjects` and `JBPermissions` contracts are deployed.
|
|
13
|
+
|
|
14
|
+
### Steps
|
|
15
|
+
|
|
16
|
+
1. **Developer deploys a contract inheriting `JBOwnable`**
|
|
17
|
+
|
|
18
|
+
```solidity
|
|
19
|
+
new MyHook(permissions, projects, address(0), projectId)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `initialOwner = address(0)` because ownership is project-based
|
|
23
|
+
- `initialProjectIdOwner = projectId`
|
|
24
|
+
|
|
25
|
+
2. **Constructor execution in `JBOwnableOverrides`**
|
|
26
|
+
|
|
27
|
+
- Stores `PROJECTS = projects` (immutable)
|
|
28
|
+
- Validates: `initialProjectIdOwner != 0` AND `address(projects) != address(0)` -- passes
|
|
29
|
+
- Validates: not both zero (passes because `initialProjectIdOwner != 0`)
|
|
30
|
+
- Calls `_transferOwnership(address(0), projectId)`:
|
|
31
|
+
- Sets `jbOwner = JBOwner({owner: address(0), projectId: projectId, permissionId: 0})`
|
|
32
|
+
- Calls `_emitTransferEvent(address(0), address(0), projectId)`
|
|
33
|
+
- In `JBOwnable._emitTransferEvent`: emits `OwnershipTransferred(address(0), PROJECTS.ownerOf(projectId), msg.sender)`
|
|
34
|
+
|
|
35
|
+
3. **Ownership is now live**
|
|
36
|
+
|
|
37
|
+
- `owner()` calls `PROJECTS.ownerOf(projectId)` and returns the current NFT holder
|
|
38
|
+
- `_checkOwner()` validates `msg.sender` against the NFT holder (or permission delegates)
|
|
39
|
+
|
|
40
|
+
### Result
|
|
41
|
+
|
|
42
|
+
The contract is owned by whichever address holds the project NFT. If the NFT is transferred, ownership automatically follows -- no on-chain update to the JBOwnable contract is needed.
|
|
43
|
+
|
|
44
|
+
### What to verify
|
|
45
|
+
|
|
46
|
+
- `jbOwner.owner == address(0)` and `jbOwner.projectId == projectId` after construction.
|
|
47
|
+
- `jbOwner.permissionId == 0` (no delegated access until explicitly configured).
|
|
48
|
+
- `owner()` returns the current NFT holder, not a cached value.
|
|
49
|
+
- If the project does not exist (ID > `PROJECTS.count()`), the constructor still succeeds -- the existence check is only enforced in `transferOwnershipToProject`, not the constructor. Verify whether this is safe (the deployer presumably knows the project exists).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Journey 2: Transfer Ownership to a Different Address
|
|
54
|
+
|
|
55
|
+
**Actor:** Current owner (direct address or project NFT holder).
|
|
56
|
+
**Goal:** Transfer ownership from the current owner to a new direct address.
|
|
57
|
+
|
|
58
|
+
### Precondition
|
|
59
|
+
|
|
60
|
+
The caller is the current owner or has the configured `permissionId` (or ROOT) via `JBPermissions`.
|
|
61
|
+
|
|
62
|
+
### Steps
|
|
63
|
+
|
|
64
|
+
1. **Owner calls `transferOwnership(newOwner)`**
|
|
65
|
+
|
|
66
|
+
- `newOwner` must not be `address(0)` (reverts with `JBOwnableOverrides_InvalidNewOwner`)
|
|
67
|
+
|
|
68
|
+
2. **`_checkOwner()` validates the caller**
|
|
69
|
+
|
|
70
|
+
- Resolves the current owner (via `PROJECTS.ownerOf()` if project-owned, or `jbOwner.owner` if address-owned)
|
|
71
|
+
- Calls `_requirePermissionFrom(resolvedOwner, projectId, permissionId)`
|
|
72
|
+
- Passes if `msg.sender == resolvedOwner` OR `msg.sender` has the required permission
|
|
73
|
+
|
|
74
|
+
3. **`_transferOwnership(newOwner, 0)` executes the transfer**
|
|
75
|
+
|
|
76
|
+
- Records `oldOwner` (resolved from current `jbOwner`)
|
|
77
|
+
- Overwrites `jbOwner = JBOwner({owner: newOwner, projectId: 0, permissionId: 0})`
|
|
78
|
+
- Calls `_emitTransferEvent(oldOwner, newOwner, 0)`
|
|
79
|
+
|
|
80
|
+
4. **`_emitTransferEvent` in `JBOwnable`**
|
|
81
|
+
|
|
82
|
+
- Since `newProjectId == 0`: emits `OwnershipTransferred(oldOwner, newOwner, msg.sender)`
|
|
83
|
+
|
|
84
|
+
### Result
|
|
85
|
+
|
|
86
|
+
`jbOwner.owner == newOwner`, `jbOwner.projectId == 0`, `jbOwner.permissionId == 0`. The new owner must call `setPermissionId()` to re-enable delegated access.
|
|
87
|
+
|
|
88
|
+
### What to verify
|
|
89
|
+
|
|
90
|
+
- If the contract was previously project-owned, `projectId` is now 0 (project ownership is cleared).
|
|
91
|
+
- `permissionId` is reset to 0, revoking all previously delegated permissions.
|
|
92
|
+
- The previous owner (or their delegates) can no longer call `onlyOwner` functions.
|
|
93
|
+
- `newOwner` can immediately call `onlyOwner` functions without any additional setup.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Journey 3: Transfer Ownership to a Juicebox Project
|
|
98
|
+
|
|
99
|
+
**Actor:** Current owner (direct address or project NFT holder).
|
|
100
|
+
**Goal:** Transfer ownership from the current owner to a Juicebox project, so the NFT holder becomes the new owner.
|
|
101
|
+
|
|
102
|
+
### Precondition
|
|
103
|
+
|
|
104
|
+
The target project exists (ID <= `PROJECTS.count()`). The caller is the current owner or has adequate permissions.
|
|
105
|
+
|
|
106
|
+
### Steps
|
|
107
|
+
|
|
108
|
+
1. **Owner calls `transferOwnershipToProject(projectId)`**
|
|
109
|
+
|
|
110
|
+
- Validates: `projectId != 0` (reverts with `JBOwnableOverrides_InvalidNewOwner`)
|
|
111
|
+
- Validates: `projectId <= type(uint88).max` (reverts with `JBOwnableOverrides_InvalidNewOwner`)
|
|
112
|
+
- Validates: `projectId <= PROJECTS.count()` (reverts with `JBOwnableOverrides_ProjectDoesNotExist`)
|
|
113
|
+
|
|
114
|
+
2. **`_checkOwner()` validates the caller** (same as Journey 2, Step 2)
|
|
115
|
+
|
|
116
|
+
3. **`_transferOwnership(address(0), uint88(projectId))` executes the transfer**
|
|
117
|
+
|
|
118
|
+
- Records `oldOwner` (resolved from current `jbOwner`)
|
|
119
|
+
- Overwrites `jbOwner = JBOwner({owner: address(0), projectId: uint88(projectId), permissionId: 0})`
|
|
120
|
+
- Calls `_emitTransferEvent(oldOwner, address(0), uint88(projectId))`
|
|
121
|
+
|
|
122
|
+
4. **`_emitTransferEvent` in `JBOwnable`**
|
|
123
|
+
|
|
124
|
+
- Since `newProjectId != 0`: emits `OwnershipTransferred(oldOwner, PROJECTS.ownerOf(projectId), msg.sender)`
|
|
125
|
+
|
|
126
|
+
### Result
|
|
127
|
+
|
|
128
|
+
`jbOwner.owner == address(0)`, `jbOwner.projectId == projectId`, `jbOwner.permissionId == 0`. The project NFT holder is now the owner. Ownership dynamically follows NFT transfers.
|
|
129
|
+
|
|
130
|
+
### What to verify
|
|
131
|
+
|
|
132
|
+
- The project existence check (`projectId <= PROJECTS.count()`) prevents transferring to a nonexistent project.
|
|
133
|
+
- The `uint88` cast does not truncate (the preceding `type(uint88).max` check ensures this).
|
|
134
|
+
- If the project NFT is subsequently burned (hypothetically), `owner()` returns `address(0)` and the contract is effectively renounced.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Journey 4: Delegate Access via Permission ID
|
|
139
|
+
|
|
140
|
+
**Actor:** Current owner.
|
|
141
|
+
**Goal:** Allow additional addresses to call `onlyOwner` functions through the JBPermissions system.
|
|
142
|
+
|
|
143
|
+
### Precondition
|
|
144
|
+
|
|
145
|
+
The contract has an owner. The owner wants to delegate access to one or more operators.
|
|
146
|
+
|
|
147
|
+
### Steps
|
|
148
|
+
|
|
149
|
+
1. **Owner calls `setPermissionId(permissionId)`**
|
|
150
|
+
|
|
151
|
+
- `_checkOwner()` validates the caller
|
|
152
|
+
- `_setPermissionId(permissionId)` writes `jbOwner.permissionId = permissionId`
|
|
153
|
+
- Emits `PermissionIdChanged(permissionId, msg.sender)`
|
|
154
|
+
|
|
155
|
+
2. **Owner grants the permission to operators via JBPermissions**
|
|
156
|
+
|
|
157
|
+
- `permissions.setPermissionsFor(account, JBPermissionsData({operator: operatorAddress, projectId: projectId, permissionIds: [permissionId]}))`
|
|
158
|
+
- This is an external call on the JBPermissions contract, not on the JBOwnable contract
|
|
159
|
+
|
|
160
|
+
3. **Operator calls an `onlyOwner` function**
|
|
161
|
+
|
|
162
|
+
- `_checkOwner()` resolves the owner and calls `_requirePermissionFrom(resolvedOwner, projectId, permissionId)`
|
|
163
|
+
- `JBPermissioned._requirePermissionFrom` checks `JBPermissions.hasPermission(msg.sender, resolvedOwner, projectId, permissionId)` -- passes
|
|
164
|
+
|
|
165
|
+
### Result
|
|
166
|
+
|
|
167
|
+
The operator can call any function protected by `onlyOwner` on this contract. The permission is scoped to the owner's account and project ID.
|
|
168
|
+
|
|
169
|
+
### What to verify
|
|
170
|
+
|
|
171
|
+
- `permissionId == 0` effectively disables delegation (permission ID 0 cannot be set in `JBPermissions`). Only the owner (or ROOT holders) can call `onlyOwner` functions.
|
|
172
|
+
- If the owner transfers ownership, `permissionId` resets to 0. The new owner must re-configure delegation.
|
|
173
|
+
- ROOT (permission ID 1) always grants access regardless of the configured `permissionId`. This is a feature of `JBPermissioned`, not specific to `JBOwnable`.
|
|
174
|
+
- 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.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Journey 5: Renounce Ownership
|
|
179
|
+
|
|
180
|
+
**Actor:** Current owner.
|
|
181
|
+
**Goal:** Permanently give up ownership, making `onlyOwner` functions uncallable.
|
|
182
|
+
|
|
183
|
+
### Precondition
|
|
184
|
+
|
|
185
|
+
The caller is the current owner and understands this action is irreversible.
|
|
186
|
+
|
|
187
|
+
### Steps
|
|
188
|
+
|
|
189
|
+
1. **Owner calls `renounceOwnership()`**
|
|
190
|
+
|
|
191
|
+
- `_checkOwner()` validates the caller
|
|
192
|
+
|
|
193
|
+
2. **`_transferOwnership(address(0), 0)` executes**
|
|
194
|
+
|
|
195
|
+
- Records `oldOwner` (resolved from current `jbOwner`)
|
|
196
|
+
- Overwrites `jbOwner = JBOwner({owner: address(0), projectId: 0, permissionId: 0})`
|
|
197
|
+
- Calls `_emitTransferEvent(oldOwner, address(0), 0)`
|
|
198
|
+
- Emits `OwnershipTransferred(oldOwner, address(0), msg.sender)`
|
|
199
|
+
|
|
200
|
+
### Result
|
|
201
|
+
|
|
202
|
+
`jbOwner` is zeroed out. `owner()` returns `address(0)`. All future calls to `_checkOwner()` revert because `_requirePermissionFrom(address(0), 0, 0)` fails for any `msg.sender` (no address equals `address(0)`, and no permission can satisfy the check against a zero-address account).
|
|
203
|
+
|
|
204
|
+
### What to verify
|
|
205
|
+
|
|
206
|
+
- After renouncing, `transferOwnership`, `transferOwnershipToProject`, `setPermissionId`, and `renounceOwnership` all revert.
|
|
207
|
+
- There is no recovery mechanism. No admin backdoor. No timelock. Renouncement is permanent.
|
|
208
|
+
- A second call to `renounceOwnership()` also reverts (because `_checkOwner()` fails).
|
|
209
|
+
- Even ROOT holders cannot act as owner after renouncement, because `_requirePermissionFrom(address(0), 0, 0)` does not recognize ROOT as a valid bypass when the account is `address(0)`.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Journey 6: Implicit Renouncement via Project NFT Burn
|
|
214
|
+
|
|
215
|
+
**Actor:** None (system behavior).
|
|
216
|
+
**Goal:** Understand what happens when the project NFT underlying a project-owned contract ceases to exist.
|
|
217
|
+
|
|
218
|
+
### Precondition
|
|
219
|
+
|
|
220
|
+
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).
|
|
221
|
+
|
|
222
|
+
### Steps
|
|
223
|
+
|
|
224
|
+
1. **`PROJECTS.ownerOf(projectId)` starts reverting**
|
|
225
|
+
|
|
226
|
+
- The ERC-721 `ownerOf` function reverts for burned tokens
|
|
227
|
+
|
|
228
|
+
2. **`owner()` catches the revert and returns `address(0)`**
|
|
229
|
+
|
|
230
|
+
- The try-catch in `owner()` returns `address(0)` when `ownerOf` reverts
|
|
231
|
+
|
|
232
|
+
3. **`_checkOwner()` catches the revert and resolves owner to `address(0)`**
|
|
233
|
+
|
|
234
|
+
- `_requirePermissionFrom(address(0), projectId, permissionId)` is called
|
|
235
|
+
- No `msg.sender` can equal `address(0)`, so the check always fails
|
|
236
|
+
|
|
237
|
+
### Result
|
|
238
|
+
|
|
239
|
+
The contract is effectively renounced without anyone calling `renounceOwnership()`. All `onlyOwner` functions permanently revert. The `jbOwner` struct still contains the old `projectId`, but it has no practical effect.
|
|
240
|
+
|
|
241
|
+
### What to verify
|
|
242
|
+
|
|
243
|
+
- There is no way to "revive" ownership after the NFT is burned. Even re-minting an NFT with the same ID (if possible) would restore ownership.
|
|
244
|
+
- The `jbOwner` struct is NOT cleared in this scenario -- it still shows the old `projectId`. Only the resolved owner is `address(0)`.
|
|
245
|
+
- This behavior is consistent between `owner()` and `_checkOwner()` (both use the same try-catch pattern).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/ownable-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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.17",
|
|
14
|
+
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
15
15
|
"@openzeppelin/contracts": "^5.6.1"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
package/src/JBOwnable.sol
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Juicebox variation on OpenZeppelin Ownable
|
|
3
|
-
pragma solidity
|
|
3
|
+
pragma solidity 0.8.26;
|
|
4
4
|
|
|
5
5
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
6
6
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
@@ -58,6 +58,9 @@ contract JBOwnable is JBOwnableOverrides {
|
|
|
58
58
|
/// @notice Either `newOwner` or `newProjectId` is non-zero or both are zero. But they can never both be non-zero.
|
|
59
59
|
/// @dev This function exists because some contracts need to deploy contracts for a project before the project's NFT
|
|
60
60
|
/// has been minted, so the transfer event resolves the project's current owner at emission time.
|
|
61
|
+
/// @dev Unlike `_transferOwnership` (which uses try-catch to resolve the *old* owner in case its project NFT was
|
|
62
|
+
/// burned), this function intentionally lets `PROJECTS.ownerOf(newProjectId)` revert if the new project doesn't
|
|
63
|
+
/// exist. A revert here is desirable — it prevents transferring ownership to a non-existent project.
|
|
61
64
|
function _emitTransferEvent(
|
|
62
65
|
address previousOwner,
|
|
63
66
|
address newOwner,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Juicebox variation on OpenZeppelin Ownable
|
|
3
|
-
pragma solidity
|
|
3
|
+
pragma solidity 0.8.26;
|
|
4
4
|
|
|
5
5
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
6
6
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
@@ -75,7 +75,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
75
75
|
revert JBOwnableOverrides_InvalidNewOwner();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// No explicit project existence check here — if `initialProjectIdOwner` refers to an unminted project,
|
|
79
|
+
// `owner()` will resolve via `PROJECTS.ownerOf()`, which reverts for non-existent tokens. The try-catch
|
|
80
|
+
// in `owner()` treats this as renounced (returns address(0)), effectively locking the contract until
|
|
81
|
+
// the project is minted. This is acceptable because deployers control the constructor arguments.
|
|
82
|
+
_transferOwnership({newOwner: initialOwner, projectId: initialProjectIdOwner});
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
//*********************************************************************//
|
|
@@ -140,7 +144,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
140
144
|
/// @dev This can only be called by the current owner.
|
|
141
145
|
function renounceOwnership() public virtual override {
|
|
142
146
|
_checkOwner();
|
|
143
|
-
_transferOwnership(address(0), 0);
|
|
147
|
+
_transferOwnership({newOwner: address(0), projectId: 0});
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
/// @notice Sets the permission ID the owner can use to give other addresses owner access.
|
|
@@ -162,7 +166,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
162
166
|
revert JBOwnableOverrides_InvalidNewOwner();
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
_transferOwnership(newOwner, 0);
|
|
169
|
+
_transferOwnership({newOwner: newOwner, projectId: 0});
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
/// @notice Transfer ownership of this contract to a new Juicebox project.
|
|
@@ -181,7 +185,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
181
185
|
revert JBOwnableOverrides_ProjectDoesNotExist();
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
|
|
188
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
189
|
+
_transferOwnership({newOwner: address(0), projectId: uint88(projectId)});
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
//*********************************************************************//
|
|
@@ -207,7 +212,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
207
212
|
/// @notice Helper to allow for drop-in replacement of OpenZeppelin `Ownable`.
|
|
208
213
|
/// @param newOwner The address that should receive ownership of this contract.
|
|
209
214
|
function _transferOwnership(address newOwner) internal virtual {
|
|
210
|
-
_transferOwnership(newOwner, 0);
|
|
215
|
+
_transferOwnership({newOwner: newOwner, projectId: 0});
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
/// @notice Transfers this contract's ownership to an address (`newOwner`) OR a Juicebox project (`projectId`).
|
|
@@ -238,6 +243,6 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
238
243
|
// This is to prevent permissions clashes for the new user/owner.
|
|
239
244
|
jbOwner = JBOwner({owner: newOwner, projectId: projectId, permissionId: 0});
|
|
240
245
|
// Emit a transfer event with the new owner's address.
|
|
241
|
-
_emitTransferEvent(oldOwner, newOwner, projectId);
|
|
246
|
+
_emitTransferEvent({previousOwner: oldOwner, newOwner: newOwner, newProjectId: projectId});
|
|
242
247
|
}
|
|
243
248
|
}
|
package/src/structs/JBOwner.sol
CHANGED
|
@@ -7,6 +7,7 @@ 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)
|
|
10
11
|
struct JBOwner {
|
|
11
12
|
address owner;
|
|
12
13
|
uint88 projectId;
|
package/test/Ownable.t.sol
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import "forge-std/Test.sol";
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
6
6
|
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
7
7
|
|
|
@@ -13,8 +13,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
|
|
|
13
13
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
14
14
|
|
|
15
15
|
contract OwnableTest is Test {
|
|
16
|
-
IJBProjects
|
|
17
|
-
IJBPermissions
|
|
16
|
+
IJBProjects projects;
|
|
17
|
+
IJBPermissions permissions;
|
|
18
18
|
|
|
19
19
|
modifier isNotContract(address a) {
|
|
20
20
|
uint256 size;
|
|
@@ -27,9 +27,9 @@ contract OwnableTest is Test {
|
|
|
27
27
|
|
|
28
28
|
function setUp() public {
|
|
29
29
|
// Deploy the permissions contract.
|
|
30
|
-
|
|
30
|
+
permissions = new JBPermissions(address(0));
|
|
31
31
|
// Deploy the projects contract.
|
|
32
|
-
|
|
32
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function testDeployerDoesNotBecomeOwner(address deployer, address owner) public isNotContract(owner) {
|
|
@@ -37,7 +37,7 @@ contract OwnableTest is Test {
|
|
|
37
37
|
vm.assume(owner != address(0));
|
|
38
38
|
|
|
39
39
|
vm.prank(deployer);
|
|
40
|
-
MockOwnable ownable = new MockOwnable(
|
|
40
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
|
|
41
41
|
|
|
42
42
|
assertEq(owner, ownable.owner(), "Deployer did not become the owner.");
|
|
43
43
|
}
|
|
@@ -56,17 +56,18 @@ contract OwnableTest is Test {
|
|
|
56
56
|
vm.assume(newProjectOwner != address(0));
|
|
57
57
|
|
|
58
58
|
// Create a project for the owner.
|
|
59
|
-
uint256 projectId =
|
|
59
|
+
uint256 projectId = projects.createFor(projectOwner);
|
|
60
60
|
|
|
61
61
|
// Create the `Ownable` contract.
|
|
62
|
-
|
|
62
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
63
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
63
64
|
|
|
64
65
|
// Make sure the deployer owns it.
|
|
65
66
|
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
66
67
|
|
|
67
68
|
// Transfer the project's ownership.
|
|
68
69
|
vm.prank(projectOwner);
|
|
69
|
-
|
|
70
|
+
projects.transferFrom(projectOwner, newProjectOwner, projectId);
|
|
70
71
|
|
|
71
72
|
// Make sure the `Ownable` contract has also been transferred to the new project owner.
|
|
72
73
|
assertEq(newProjectOwner, ownable.owner(), "Ownable did not follow the Project owner.");
|
|
@@ -86,10 +87,11 @@ contract OwnableTest is Test {
|
|
|
86
87
|
vm.assume(projectOwner != address(0));
|
|
87
88
|
|
|
88
89
|
// Create a project for the owner.
|
|
89
|
-
uint256 _projectId =
|
|
90
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
90
91
|
|
|
91
92
|
// Create the `Ownable` contract.
|
|
92
|
-
|
|
93
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
94
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
93
95
|
|
|
94
96
|
// Make sure the project owner owns it.
|
|
95
97
|
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
@@ -100,7 +102,7 @@ contract OwnableTest is Test {
|
|
|
100
102
|
// Make sure it was transferred to the new owner.
|
|
101
103
|
assertEq(newOwnableOwner, ownable.owner());
|
|
102
104
|
// Sanity check to make sure it only the `Ownable` changed, and that the project did not.
|
|
103
|
-
assertEq(
|
|
105
|
+
assertEq(projects.ownerOf(_projectId), projectOwner);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
function testCantTransferToProjectZero(address owner) public {
|
|
@@ -108,7 +110,7 @@ contract OwnableTest is Test {
|
|
|
108
110
|
vm.startPrank(owner);
|
|
109
111
|
|
|
110
112
|
// Create the `Ownable` contract.
|
|
111
|
-
MockOwnable ownable = new MockOwnable(
|
|
113
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, 0);
|
|
112
114
|
|
|
113
115
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
114
116
|
|
|
@@ -122,7 +124,7 @@ contract OwnableTest is Test {
|
|
|
122
124
|
vm.startPrank(owner);
|
|
123
125
|
|
|
124
126
|
// Create the `Ownable` contract.
|
|
125
|
-
MockOwnable ownable = new MockOwnable(
|
|
127
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
|
|
126
128
|
|
|
127
129
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
128
130
|
|
|
@@ -145,18 +147,19 @@ contract OwnableTest is Test {
|
|
|
145
147
|
vm.assume(newProjectOwner != address(0));
|
|
146
148
|
|
|
147
149
|
// Create a project for the owner.
|
|
148
|
-
uint256 _projectId =
|
|
150
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
149
151
|
|
|
150
152
|
// Create the `Ownable` contract.
|
|
151
|
-
|
|
153
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
154
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
152
155
|
|
|
153
156
|
// Make sure the project owner owns it.
|
|
154
157
|
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
155
158
|
|
|
156
159
|
// Transfer the project ownership.
|
|
157
160
|
vm.prank(projectOwner);
|
|
158
|
-
|
|
159
|
-
assertEq(
|
|
161
|
+
projects.transferFrom(projectOwner, newProjectOwner, _projectId);
|
|
162
|
+
assertEq(projects.ownerOf(_projectId), newProjectOwner);
|
|
160
163
|
|
|
161
164
|
// Make sure the `Ownable` contract has also been transferred to the new project owner.
|
|
162
165
|
assertEq(newProjectOwner, ownable.owner());
|
|
@@ -167,7 +170,7 @@ contract OwnableTest is Test {
|
|
|
167
170
|
vm.assume(deployer != owner);
|
|
168
171
|
|
|
169
172
|
// Create the `Ownable` contract.
|
|
170
|
-
MockOwnable ownable = new MockOwnable(
|
|
173
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
|
|
171
174
|
|
|
172
175
|
// Transfer ownership to the project owner.
|
|
173
176
|
vm.prank(owner);
|
|
@@ -185,11 +188,12 @@ contract OwnableTest is Test {
|
|
|
185
188
|
vm.assume(projectOwner != address(0));
|
|
186
189
|
|
|
187
190
|
// Create a project for the owner.
|
|
188
|
-
uint256 _projectId =
|
|
191
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
189
192
|
|
|
190
193
|
// Create the `Ownable` contract.
|
|
191
194
|
vm.prank(deployer);
|
|
192
|
-
|
|
195
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
196
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
193
197
|
|
|
194
198
|
// Renounce the ownership.
|
|
195
199
|
vm.prank(projectOwner);
|
|
@@ -218,10 +222,11 @@ contract OwnableTest is Test {
|
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
// Create a project for the owner.
|
|
221
|
-
uint256 _projectId =
|
|
225
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
222
226
|
|
|
223
227
|
// Create the `Ownable` contract.
|
|
224
|
-
|
|
228
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
229
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
225
230
|
|
|
226
231
|
// Set the required permission.
|
|
227
232
|
vm.prank(projectOwner);
|
|
@@ -254,8 +259,9 @@ contract OwnableTest is Test {
|
|
|
254
259
|
|
|
255
260
|
// The owner gives permission to the caller.
|
|
256
261
|
vm.prank(projectOwner);
|
|
257
|
-
|
|
262
|
+
permissions.setPermissionsFor(
|
|
258
263
|
projectOwner,
|
|
264
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
259
265
|
JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
|
|
260
266
|
);
|
|
261
267
|
|
|
@@ -296,10 +302,11 @@ contract OwnableTest is Test {
|
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
// Create a project for the owner.
|
|
299
|
-
uint256 _projectId =
|
|
305
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
300
306
|
|
|
301
307
|
// Create the `Ownable` contract.
|
|
302
|
-
|
|
308
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
309
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
303
310
|
|
|
304
311
|
// Set the permission that is required.
|
|
305
312
|
ownable.setPermission(requiredPermissionId);
|
|
@@ -331,8 +338,9 @@ contract OwnableTest is Test {
|
|
|
331
338
|
|
|
332
339
|
// The owner gives permission to the caller.
|
|
333
340
|
vm.prank(projectOwner);
|
|
334
|
-
|
|
341
|
+
permissions.setPermissionsFor(
|
|
335
342
|
projectOwner,
|
|
343
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
336
344
|
JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
|
|
337
345
|
);
|
|
338
346
|
|
|
@@ -356,19 +364,20 @@ contract OwnableTest is Test {
|
|
|
356
364
|
vm.assume(owner != address(0) && projectOwner != address(0));
|
|
357
365
|
|
|
358
366
|
// Create a project for the owner.
|
|
359
|
-
uint256 _projectId =
|
|
367
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
360
368
|
|
|
361
369
|
// Should revert because we set both a owner and a projectOwner
|
|
362
370
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
363
371
|
|
|
364
372
|
// Create the `Ownable` contract.
|
|
365
|
-
|
|
373
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
374
|
+
new MockOwnable(projects, permissions, address(owner), uint88(_projectId));
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
function testCantInitializeAsRenounced() public {
|
|
369
378
|
// Should revert because we set both a owner and a projectOwner
|
|
370
379
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
371
380
|
// Create the `Ownable` contract.
|
|
372
|
-
new MockOwnable(
|
|
381
|
+
new MockOwnable(projects, permissions, address(0), uint88(0));
|
|
373
382
|
}
|
|
374
383
|
}
|