@bananapus/ownable-v6 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/foundry.toml +1 -1
- package/package.json +13 -4
- package/src/JBOwnable.sol +5 -10
- package/src/JBOwnableOverrides.sol +29 -23
- package/src/interfaces/IJBOwnable.sol +7 -5
- package/src/structs/JBOwner.sol +6 -6
- package/ADMINISTRATION.md +0 -79
- package/ARCHITECTURE.md +0 -92
- package/AUDIT_INSTRUCTIONS.md +0 -69
- package/RISKS.md +0 -51
- package/SKILLS.md +0 -41
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -117
- package/slither-ci.config.json +0 -10
- package/test/CodexUnmintedProjectHijack.t.sol +0 -45
- package/test/Ownable.t.sol +0 -383
- package/test/OwnableAttacks.t.sol +0 -190
- package/test/OwnableEdgeCases.t.sol +0 -437
- package/test/OwnableInvariantTests.sol +0 -48
- package/test/audit/PermissionDriftAfterProjectTransfer.t.sol +0 -58
- package/test/audit/PermissionIdNFTTransfer.t.sol +0 -93
- package/test/audit/ProjectTransferPermissionPolicy.t.sol +0 -58
- package/test/handlers/OwnableHandler.sol +0 -75
- package/test/regression/BurnLockProtection.t.sol +0 -112
- package/test/regression/RootPermissionBypassesPermissionIdZero.t.sol +0 -87
- package/test/regression/ZeroAddressValidation.t.sol +0 -67
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/ownable-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Bananapus/nana-ownable-v6"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"foundry.lock",
|
|
12
|
+
"foundry.toml",
|
|
13
|
+
"references/",
|
|
14
|
+
"remappings.txt",
|
|
15
|
+
"src/",
|
|
16
|
+
"test/mocks/"
|
|
17
|
+
],
|
|
9
18
|
"engines": {
|
|
10
19
|
"node": ">=20.0.0"
|
|
11
20
|
},
|
|
12
21
|
"dependencies": {
|
|
13
|
-
"@bananapus/core-v6": "
|
|
14
|
-
"@bananapus/permission-ids-v6": "
|
|
15
|
-
"@openzeppelin/contracts": "
|
|
22
|
+
"@bananapus/core-v6": "0.0.38",
|
|
23
|
+
"@bananapus/permission-ids-v6": "0.0.22",
|
|
24
|
+
"@openzeppelin/contracts": "5.6.1"
|
|
16
25
|
},
|
|
17
26
|
"scripts": {
|
|
18
27
|
"test": "forge test",
|
package/src/JBOwnable.sol
CHANGED
|
@@ -7,16 +7,11 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
|
|
|
7
7
|
|
|
8
8
|
import {JBOwnableOverrides} from "./JBOwnableOverrides.sol";
|
|
9
9
|
|
|
10
|
-
/// @notice
|
|
11
|
-
///
|
|
12
|
-
/// @dev
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
/// the owner.
|
|
16
|
-
/// 2. If `JBOwner.projectId` is set to `0`, the `JBOwner.owner` address is the owner.
|
|
17
|
-
/// 3. The owner can give other addresses access with `JBPermissions.setPermissionsFor(...)`, using the
|
|
18
|
-
/// `JBOwner.permissionId` permission.
|
|
19
|
-
/// @dev To use `onlyOwner`, inherit this contract and apply the modifier to a function.
|
|
10
|
+
/// @notice Juicebox-aware ownership for any contract. Inherit this and apply the `onlyOwner` modifier to restrict
|
|
11
|
+
/// functions to the project owner, a fixed address, or anyone the owner has granted permission to via `JBPermissions`.
|
|
12
|
+
/// @dev Ownership resolves dynamically: if `JBOwner.projectId` is set, the holder of that project's ERC-721 NFT is
|
|
13
|
+
/// the owner. If `projectId` is 0, the stored `JBOwner.owner` address is used instead. The owner can delegate access
|
|
14
|
+
/// to other addresses by setting a `permissionId` and granting that permission through `JBPermissions`.
|
|
20
15
|
contract JBOwnable is JBOwnableOverrides {
|
|
21
16
|
//*********************************************************************//
|
|
22
17
|
// -------------------------- constructor ---------------------------- //
|
|
@@ -10,9 +10,12 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
|
10
10
|
import {IJBOwnable} from "./interfaces/IJBOwnable.sol";
|
|
11
11
|
import {JBOwner} from "./structs/JBOwner.sol";
|
|
12
12
|
|
|
13
|
-
/// @notice
|
|
14
|
-
///
|
|
15
|
-
/// `JBPermissions`.
|
|
13
|
+
/// @notice Abstract base implementing Juicebox-aware ownership resolution, transfer, and permission delegation.
|
|
14
|
+
/// Ownership is either address-based (a fixed EOA/contract) or project-based (whoever holds the project's ERC-721
|
|
15
|
+
/// NFT). The owner can delegate access to other addresses by configuring a `permissionId` in `JBPermissions`.
|
|
16
|
+
/// @dev Stale permission detection: when ownership changes (e.g. project NFT transferred), the `permissionId` is
|
|
17
|
+
/// effectively ignored until the new owner explicitly re-sets it — preventing the previous owner's delegates from
|
|
18
|
+
/// retaining access.
|
|
16
19
|
abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
17
20
|
//*********************************************************************//
|
|
18
21
|
// --------------------------- custom errors ------------------------- //
|
|
@@ -26,14 +29,14 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
26
29
|
// ---------------- public immutable stored properties --------------- //
|
|
27
30
|
//*********************************************************************//
|
|
28
31
|
|
|
29
|
-
/// @notice
|
|
32
|
+
/// @notice The `JBProjects` ERC-721 contract used to resolve project-based ownership.
|
|
30
33
|
IJBProjects public immutable override PROJECTS;
|
|
31
34
|
|
|
32
35
|
//*********************************************************************//
|
|
33
36
|
// --------------------- public stored properties -------------------- //
|
|
34
37
|
//*********************************************************************//
|
|
35
38
|
|
|
36
|
-
/// @notice
|
|
39
|
+
/// @notice The current ownership state — who owns this contract and how permission delegation is configured.
|
|
37
40
|
JBOwner public override jbOwner;
|
|
38
41
|
|
|
39
42
|
//*********************************************************************//
|
|
@@ -98,12 +101,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
98
101
|
// -------------------------- public views --------------------------- //
|
|
99
102
|
//*********************************************************************//
|
|
100
103
|
|
|
101
|
-
/// @notice Returns the owner's address based
|
|
102
|
-
///
|
|
103
|
-
///
|
|
104
|
-
///
|
|
105
|
-
///
|
|
106
|
-
/// unexpected ERC-721 behavior.
|
|
104
|
+
/// @notice Returns the current owner's address. If ownership is project-based, this dynamically resolves to
|
|
105
|
+
/// whoever holds the project's ERC-721 NFT right now.
|
|
106
|
+
/// @dev If `projectId` is non-zero, resolves via `PROJECTS.ownerOf()`. If that call reverts (e.g., burned NFT),
|
|
107
|
+
/// returns `address(0)` — effectively treating the contract as renounced. `JBProjects` V6 has no burn function,
|
|
108
|
+
/// so this is a defensive measure only.
|
|
107
109
|
function owner() public view virtual returns (address) {
|
|
108
110
|
JBOwner memory ownerInfo = jbOwner;
|
|
109
111
|
|
|
@@ -124,9 +126,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
124
126
|
// -------------------------- internal views ------------------------- //
|
|
125
127
|
//*********************************************************************//
|
|
126
128
|
|
|
127
|
-
/// @notice Reverts if the
|
|
129
|
+
/// @notice Reverts if the caller is not the owner (or an authorized delegate when `permissionId` is set).
|
|
128
130
|
/// @dev If `projectId` is non-zero and `PROJECTS.ownerOf()` reverts (e.g., burned NFT), the resolved owner is
|
|
129
131
|
/// `address(0)`, causing all `_checkOwner` calls to revert — equivalent to a renounced contract.
|
|
132
|
+
/// @dev Stale permission detection: if the resolved owner differs from `_permissionOwner` (set when
|
|
133
|
+
/// `setPermissionId` was last called), delegation is disabled until the new owner re-configures it.
|
|
130
134
|
function _checkOwner() internal view virtual {
|
|
131
135
|
JBOwner memory ownerInfo = jbOwner;
|
|
132
136
|
|
|
@@ -169,17 +173,18 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
169
173
|
// ---------------------- public transactions ------------------------ //
|
|
170
174
|
//*********************************************************************//
|
|
171
175
|
|
|
172
|
-
/// @notice
|
|
173
|
-
///
|
|
174
|
-
/// @dev This can only be called by the current owner.
|
|
176
|
+
/// @notice Permanently gives up ownership. After this, no address can call `onlyOwner` functions.
|
|
177
|
+
/// @dev Can only be called by the current owner. This is irreversible.
|
|
175
178
|
function renounceOwnership() public virtual override {
|
|
176
179
|
_checkOwner();
|
|
177
180
|
_transferOwnership({newOwner: address(0), projectId: 0});
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
/// @notice
|
|
181
|
-
///
|
|
182
|
-
/// @
|
|
183
|
+
/// @notice Configures which `JBPermissions` permission ID grants delegate access to `onlyOwner` functions.
|
|
184
|
+
/// Set to 0 to disable delegation entirely (only the direct owner can call).
|
|
185
|
+
/// @dev Can only be called by the current owner. Records the current owner so stale permissions are detected
|
|
186
|
+
/// if ownership later changes.
|
|
187
|
+
/// @param permissionId The permission ID to use for `onlyOwner` delegation.
|
|
183
188
|
function setPermissionId(uint8 permissionId) public virtual override {
|
|
184
189
|
_checkOwner();
|
|
185
190
|
_setPermissionId(permissionId);
|
|
@@ -199,10 +204,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
199
204
|
_transferOwnership({newOwner: newOwner, projectId: 0});
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
/// @notice
|
|
203
|
-
///
|
|
204
|
-
/// The
|
|
205
|
-
///
|
|
207
|
+
/// @notice Transfers ownership to a Juicebox project — whoever holds that project's ERC-721 NFT becomes the
|
|
208
|
+
/// owner.
|
|
209
|
+
/// @dev The `permissionId` is reset to 0 on transfer to prevent the previous owner's delegates from retaining
|
|
210
|
+
/// access. The new project owner must call `setPermissionId()` to re-enable delegation.
|
|
211
|
+
/// @dev The `projectId` must fit within a `uint88` and the project must already exist.
|
|
206
212
|
/// @param projectId The ID of the project to transfer ownership to.
|
|
207
213
|
function transferOwnershipToProject(uint256 projectId) public virtual override {
|
|
208
214
|
_checkOwner();
|
|
@@ -240,7 +246,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
240
246
|
emit PermissionIdChanged({newId: permissionId, caller: _msgSender()});
|
|
241
247
|
}
|
|
242
248
|
|
|
243
|
-
/// @notice
|
|
249
|
+
/// @notice Drop-in replacement for OpenZeppelin's `Ownable._transferOwnership(address)`.
|
|
244
250
|
/// @param newOwner The address that should receive ownership of this contract.
|
|
245
251
|
function _transferOwnership(address newOwner) internal virtual {
|
|
246
252
|
_transferOwnership({newOwner: newOwner, projectId: 0});
|
|
@@ -3,7 +3,9 @@ pragma solidity ^0.8.0;
|
|
|
3
3
|
|
|
4
4
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
5
5
|
|
|
6
|
-
/// @notice
|
|
6
|
+
/// @notice Interface for Juicebox-aware ownership. Supports two modes: address-based (a fixed EOA/contract owns the
|
|
7
|
+
/// contract) or project-based (whoever holds a specific Juicebox project's ERC-721 NFT is the owner). The owner can
|
|
8
|
+
/// delegate access to other addresses via `JBPermissions`.
|
|
7
9
|
interface IJBOwnable {
|
|
8
10
|
/// @notice Emitted when ownership is transferred to a new owner.
|
|
9
11
|
/// @param previousOwner The address of the previous owner.
|
|
@@ -16,14 +18,14 @@ interface IJBOwnable {
|
|
|
16
18
|
/// @param caller The address that changed the permission ID.
|
|
17
19
|
event PermissionIdChanged(uint8 newId, address caller);
|
|
18
20
|
|
|
19
|
-
/// @notice The contract
|
|
21
|
+
/// @notice The `JBProjects` ERC-721 contract used to resolve project-based ownership.
|
|
20
22
|
/// @return projects The `IJBProjects` contract.
|
|
21
23
|
function PROJECTS() external view returns (IJBProjects projects);
|
|
22
24
|
|
|
23
|
-
/// @notice
|
|
25
|
+
/// @notice The current ownership state — who owns this contract and how permission delegation is configured.
|
|
24
26
|
/// @return owner The owner address (used when `projectId` is 0).
|
|
25
|
-
/// @return projectId The
|
|
26
|
-
/// @return permissionId The permission ID
|
|
27
|
+
/// @return projectId The Juicebox project whose NFT holder is the owner (0 if address-based ownership).
|
|
28
|
+
/// @return permissionId The permission ID that delegates can use to act as owner via `JBPermissions`.
|
|
27
29
|
function jbOwner() external view returns (address owner, uint88 projectId, uint8 permissionId);
|
|
28
30
|
|
|
29
31
|
/// @notice Returns the current owner's address.
|
package/src/structs/JBOwner.sol
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
/// @notice
|
|
5
|
-
/// @custom:member owner
|
|
6
|
-
/// @custom:member projectId
|
|
7
|
-
/// `owner`
|
|
8
|
-
/// @custom:member permissionId The permission ID
|
|
9
|
-
///
|
|
4
|
+
/// @notice Describes who owns a `JBOwnable` contract and how they can delegate access.
|
|
5
|
+
/// @custom:member owner The owner address — only used when `projectId` is 0 (address-based ownership mode).
|
|
6
|
+
/// @custom:member projectId If non-zero, the holder of this Juicebox project's ERC-721 NFT is the owner. When set,
|
|
7
|
+
/// the `owner` field is ignored and ownership resolves dynamically via `JBProjects.ownerOf(projectId)`.
|
|
8
|
+
/// @custom:member permissionId The permission ID that delegates can hold (via `JBPermissions`) to act as owner. Set
|
|
9
|
+
/// to 0 to disable delegation entirely — only the direct owner can call `onlyOwner` functions.
|
|
10
10
|
struct JBOwner {
|
|
11
11
|
address owner;
|
|
12
12
|
uint88 projectId;
|
package/ADMINISTRATION.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Administration
|
|
2
|
-
|
|
3
|
-
## At A Glance
|
|
4
|
-
|
|
5
|
-
| Item | Details |
|
|
6
|
-
| --- | --- |
|
|
7
|
-
| Scope | Ownership resolution primitive used by downstream repos |
|
|
8
|
-
| Control posture | Primitive only; control depends on the inheriting contract |
|
|
9
|
-
| Highest-risk actions | Transferring ownership to the wrong address or project and assuming delegated operators survive transfer |
|
|
10
|
-
| Recovery posture | Recovery depends on the inheriting contract and the still-recognized current owner |
|
|
11
|
-
|
|
12
|
-
## Purpose
|
|
13
|
-
|
|
14
|
-
`nana-ownable-v6` does not add a new admin surface by itself. It defines how ownership is resolved for other repos. The important question is how a contract's `owner()` is determined and how delegated permission IDs behave across ownership transfers.
|
|
15
|
-
|
|
16
|
-
## Control Model
|
|
17
|
-
|
|
18
|
-
- ownership can be address-based or project-based
|
|
19
|
-
- delegated operator checks run through `JBPermissions`
|
|
20
|
-
- transfer and renounce semantics are part of the primitive
|
|
21
|
-
- delegated permission resets on ownership transfer
|
|
22
|
-
|
|
23
|
-
## Roles
|
|
24
|
-
|
|
25
|
-
| Role | How Assigned | Scope | Notes |
|
|
26
|
-
| --- | --- | --- | --- |
|
|
27
|
-
| Direct owner | Stored owner address | Per contract | Standard `Ownable`-like control |
|
|
28
|
-
| Project owner | Holder of the referenced project NFT | Per contract | Dynamic ownership resolution |
|
|
29
|
-
| Delegated operator | `JBPermissions` grant with the configured permission ID | Per contract and project | Only if the inheriting contract enables it |
|
|
30
|
-
|
|
31
|
-
## Privileged Surfaces
|
|
32
|
-
|
|
33
|
-
The meaningful control surfaces are inherited by downstream contracts:
|
|
34
|
-
|
|
35
|
-
- `setPermissionId(...)`
|
|
36
|
-
- `transferOwnership(...)`
|
|
37
|
-
- `transferOwnershipToProject(...)`
|
|
38
|
-
- `renounceOwnership()`
|
|
39
|
-
- `onlyOwner` checks that resolve either the direct owner or the current project NFT holder
|
|
40
|
-
|
|
41
|
-
## Immutable And One-Way
|
|
42
|
-
|
|
43
|
-
- project ownership changes dynamically with project NFT transfers
|
|
44
|
-
- delegated permission ID resets on ownership transfer
|
|
45
|
-
- renouncing ownership is final unless the inheriting contract adds a separate recovery path
|
|
46
|
-
|
|
47
|
-
## Operational Notes
|
|
48
|
-
|
|
49
|
-
- treat project-based ownership as live routing, not a snapshot
|
|
50
|
-
- do not assume an operator permission survives ownership transfer
|
|
51
|
-
- treat `setPermissionId(...)` as a real authority change because it rewires which delegated permission bit counts as owner access
|
|
52
|
-
- review the inheriting contract, not just this primitive, to understand the full admin surface
|
|
53
|
-
|
|
54
|
-
## Machine Notes
|
|
55
|
-
|
|
56
|
-
- do not conclude authority from this repo alone; follow the inheriting contract's `onlyOwner` surfaces
|
|
57
|
-
- treat ownership transfer as potentially changing both the owner identity and the usable delegated permission ID
|
|
58
|
-
- if the current permission ID is undocumented, inspect `jbOwner.permissionId` before reasoning about delegated owner access
|
|
59
|
-
- if a downstream repo uses project-based ownership, re-evaluate owner resolution after every project NFT transfer
|
|
60
|
-
|
|
61
|
-
## Recovery
|
|
62
|
-
|
|
63
|
-
- this primitive has no protocol-wide recovery surface
|
|
64
|
-
- if ownership was transferred to the wrong project or address, recovery depends on the inheriting contract still recognizing the current owner
|
|
65
|
-
|
|
66
|
-
## Admin Boundaries
|
|
67
|
-
|
|
68
|
-
- this repo does not create a new permission namespace
|
|
69
|
-
- it cannot make an inheriting contract safer than that contract's own privileged functions
|
|
70
|
-
- it cannot preserve delegated operators across ownership transfer by default
|
|
71
|
-
|
|
72
|
-
## Source Map
|
|
73
|
-
|
|
74
|
-
- `src/JBOwnable.sol`
|
|
75
|
-
- `src/JBOwnableOverrides.sol`
|
|
76
|
-
- `src/structs/JBOwner.sol`
|
|
77
|
-
- `test/OwnableInvariantTests.sol`
|
|
78
|
-
- `test/OwnableEdgeCases.t.sol`
|
|
79
|
-
- `test/regression/BurnLockProtection.t.sol`
|
package/ARCHITECTURE.md
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
`nana-ownable-v6` adapts `Ownable` to the Juicebox model. A contract can be owned by an address or by a Juicebox project NFT, and delegated operators can satisfy `onlyOwner` through `JBPermissions`.
|
|
6
|
-
|
|
7
|
-
## System Overview
|
|
8
|
-
|
|
9
|
-
This repo is an ownership primitive, not a policy layer. `JBOwnable` gives downstream repos a familiar inheritance surface. `JBOwnableOverrides` implements dynamic owner resolution, ownership transfer, renounce behavior, and delegated permission checks.
|
|
10
|
-
|
|
11
|
-
Ownership can follow the current holder of a Juicebox project NFT instead of staying fixed to one address.
|
|
12
|
-
|
|
13
|
-
## Core Invariants
|
|
14
|
-
|
|
15
|
-
- project-owned contracts must resolve the owner dynamically from the current project NFT holder
|
|
16
|
-
- the delegated permission ID resets on ownership transfer
|
|
17
|
-
- pointing ownership at an unminted project can temporarily lock the contract until that project exists
|
|
18
|
-
- an invalid or otherwise unresolvable project NFT effectively renounces ownership
|
|
19
|
-
- this repo should stay a drop-in primitive, not grow product-specific access rules
|
|
20
|
-
|
|
21
|
-
## Modules
|
|
22
|
-
|
|
23
|
-
| Module | Responsibility | Notes |
|
|
24
|
-
| --- | --- | --- |
|
|
25
|
-
| `JBOwnable` | Familiar `onlyOwner` inheritance target | Concrete surface |
|
|
26
|
-
| `JBOwnableOverrides` | Resolution, transfer, renounce, and delegated-permission logic | Core behavior |
|
|
27
|
-
| `JBOwner` | Packed owner state | Shared struct |
|
|
28
|
-
| `IJBOwnable` | Public interface and events | Integration surface |
|
|
29
|
-
|
|
30
|
-
## Trust Boundaries
|
|
31
|
-
|
|
32
|
-
- ownership resolution depends on `JBProjects` and `JBPermissions` from `nana-core-v6`
|
|
33
|
-
- this repo does not create a new permission namespace
|
|
34
|
-
- inheriting contracts may add policy on top, but the resolution semantics here are infrastructure-level
|
|
35
|
-
|
|
36
|
-
## Critical Flows
|
|
37
|
-
|
|
38
|
-
### Owner Check
|
|
39
|
-
|
|
40
|
-
```text
|
|
41
|
-
onlyOwner modifier
|
|
42
|
-
-> load packed owner state
|
|
43
|
-
-> if project-owned, resolve the current project NFT holder
|
|
44
|
-
-> otherwise use the stored owner address
|
|
45
|
-
-> accept either the resolved owner or an operator with the configured JB permission
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Accounting Model
|
|
49
|
-
|
|
50
|
-
No treasury accounting lives here. The important state is ownership resolution data and delegated permission ID.
|
|
51
|
-
|
|
52
|
-
## Security Model
|
|
53
|
-
|
|
54
|
-
- ownership resolution edge cases matter more than surface API shape
|
|
55
|
-
- permission delegation is simple but security-sensitive because it composes with a global permission registry
|
|
56
|
-
- unresolvable project ownership is intentionally fail-closed
|
|
57
|
-
|
|
58
|
-
## Safe Change Guide
|
|
59
|
-
|
|
60
|
-
- be conservative with transfer and renounce semantics
|
|
61
|
-
- if event emission or transfer behavior changes, inspect deployer wrappers and inheriting repos
|
|
62
|
-
- if project-based ownership semantics change, re-check unminted-project and unresolvable-project behavior explicitly
|
|
63
|
-
- do not make delegated permission IDs sticky across ownership transfers
|
|
64
|
-
|
|
65
|
-
## Canonical Checks
|
|
66
|
-
|
|
67
|
-
- baseline address-owner and project-owner behavior:
|
|
68
|
-
`test/Ownable.t.sol`
|
|
69
|
-
- transfer, renounce, and hostile-call edge cases:
|
|
70
|
-
`test/OwnableEdgeCases.t.sol`
|
|
71
|
-
`test/OwnableAttacks.t.sol`
|
|
72
|
-
- unminted-project and burn-lock safety:
|
|
73
|
-
`test/CodexUnmintedProjectHijack.t.sol`
|
|
74
|
-
`test/regression/BurnLockProtection.t.sol`
|
|
75
|
-
- ownership-state invariants:
|
|
76
|
-
`test/OwnableInvariantTests.sol`
|
|
77
|
-
|
|
78
|
-
## Source Map
|
|
79
|
-
|
|
80
|
-
- `src/JBOwnable.sol`
|
|
81
|
-
- `src/JBOwnableOverrides.sol`
|
|
82
|
-
- `src/structs/JBOwner.sol`
|
|
83
|
-
- `src/interfaces/IJBOwnable.sol`
|
|
84
|
-
- `test/Ownable.t.sol`
|
|
85
|
-
- `test/OwnableEdgeCases.t.sol`
|
|
86
|
-
- `test/OwnableAttacks.t.sol`
|
|
87
|
-
- `test/CodexUnmintedProjectHijack.t.sol`
|
|
88
|
-
- `test/regression/BurnLockProtection.t.sol`
|
|
89
|
-
- `test/regression/ZeroAddressValidation.t.sol`
|
|
90
|
-
- `test/OwnableInvariantTests.sol`
|
|
91
|
-
- `references/runtime.md`
|
|
92
|
-
- `references/operations.md`
|
package/AUDIT_INSTRUCTIONS.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# Audit Instructions
|
|
2
|
-
|
|
3
|
-
This repo provides ownership helpers that can follow Juicebox project NFTs instead of a fixed EOA. It is a small repo with outsized privilege impact.
|
|
4
|
-
|
|
5
|
-
## Audit Objective
|
|
6
|
-
|
|
7
|
-
Find issues that:
|
|
8
|
-
|
|
9
|
-
- let unauthorized actors satisfy owner checks
|
|
10
|
-
- break ownership updates when a project NFT moves, burns, or locks
|
|
11
|
-
- let override logic produce a different owner than the project system intends
|
|
12
|
-
- leave dependent repos with stale or permanently wrong ownership views
|
|
13
|
-
|
|
14
|
-
## Scope
|
|
15
|
-
|
|
16
|
-
In scope:
|
|
17
|
-
|
|
18
|
-
- `src/JBOwnable.sol`
|
|
19
|
-
- `src/JBOwnableOverrides.sol`
|
|
20
|
-
- `src/interfaces/`
|
|
21
|
-
- `src/structs/`
|
|
22
|
-
|
|
23
|
-
## Start Here
|
|
24
|
-
|
|
25
|
-
1. `src/JBOwnable.sol`
|
|
26
|
-
2. `src/JBOwnableOverrides.sol`
|
|
27
|
-
|
|
28
|
-
## Security Model
|
|
29
|
-
|
|
30
|
-
These contracts abstract "owner" as a project-based identity. Downstream repos use them to:
|
|
31
|
-
|
|
32
|
-
- treat a Juicebox project owner as contract owner
|
|
33
|
-
- apply per-project override rules
|
|
34
|
-
- keep admin power aligned with project NFT ownership instead of a static address
|
|
35
|
-
|
|
36
|
-
## Roles And Privileges
|
|
37
|
-
|
|
38
|
-
| Role | Powers | How constrained |
|
|
39
|
-
|------|--------|-----------------|
|
|
40
|
-
| Project NFT owner | Become the effective contract owner | Should update automatically with NFT transfers |
|
|
41
|
-
| Override authority | Set alternative owner resolution where allowed | Must not outrank project ownership unexpectedly |
|
|
42
|
-
|
|
43
|
-
## Integration Assumptions
|
|
44
|
-
|
|
45
|
-
| Dependency | Assumption | What breaks if wrong |
|
|
46
|
-
|------------|------------|----------------------|
|
|
47
|
-
| Juicebox project ownership | NFT ownership reflects intended authority | Downstream admin checks drift from reality |
|
|
48
|
-
|
|
49
|
-
## Critical Invariants
|
|
50
|
-
|
|
51
|
-
1. Owner resolution is correct.
|
|
52
|
-
For any supported mode, `owner()` and owner checks must resolve to the intended authority and no one else.
|
|
53
|
-
2. Burn and lock behavior is safe.
|
|
54
|
-
If project ownership is intentionally burned or locked, the helper must not accidentally reopen control or brick valid admin paths.
|
|
55
|
-
3. Override precedence is coherent.
|
|
56
|
-
Overrides must not silently supersede project ownership in cases the design does not permit.
|
|
57
|
-
|
|
58
|
-
## Attack Surfaces
|
|
59
|
-
|
|
60
|
-
- owner resolution after project NFT transfer
|
|
61
|
-
- zero-address, burn, and lock states
|
|
62
|
-
- override configuration and precedence
|
|
63
|
-
- downstream assumptions that cache owner state instead of re-reading it
|
|
64
|
-
|
|
65
|
-
## Verification
|
|
66
|
-
|
|
67
|
-
- `npm install`
|
|
68
|
-
- `forge build`
|
|
69
|
-
- `forge test`
|
package/RISKS.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Juicebox Ownable Risk Register
|
|
2
|
-
|
|
3
|
-
This file covers the ownership-model risks in `JBOwnable`: dynamic ownership through project NFTs, delegated owner authority, and mismatches with standard `Ownable` expectations.
|
|
4
|
-
|
|
5
|
-
## How To Use This File
|
|
6
|
-
|
|
7
|
-
- Read `Priority risks` first. Most failures here come from authority-model mistakes, not arithmetic bugs.
|
|
8
|
-
- Use the later sections to understand what changes when ownership follows a project instead of a fixed address.
|
|
9
|
-
- Treat `Invariants to verify` as the minimum proof that owner resolution stays coherent.
|
|
10
|
-
|
|
11
|
-
## Priority Risks
|
|
12
|
-
|
|
13
|
-
| Priority | Risk | Why it matters | Primary controls |
|
|
14
|
-
|----------|------|----------------|------------------|
|
|
15
|
-
| P1 | Misunderstanding dynamic owner resolution | Ownership can move when the project NFT moves or when permissions change, which breaks static `Ownable` assumptions. | Clear docs, careful integration review, and explicit tests around transfer paths. |
|
|
16
|
-
| P1 | Over-broad delegated owner permissions | `JBPermissions` can broaden who effectively acts as owner. Bad configuration expands blast radius quickly. | Permission hygiene and explicit review of delegated grants. |
|
|
17
|
-
| P2 | Tooling assumptions about standard `Ownable` | Some tooling assumes `owner()` maps to one address with no external permission system behind it. | Integration testing and clear documentation of the semantic differences. |
|
|
18
|
-
|
|
19
|
-
## 1. Trust Assumptions
|
|
20
|
-
|
|
21
|
-
- **`JBPermissions` works correctly.** A bug there affects every `JBOwnable` contract that relies on delegated owner access.
|
|
22
|
-
- **`JBProjects` ownership is the source of truth.** When a contract is project-owned, whoever holds the project NFT has owner access.
|
|
23
|
-
- **Delegated permission means owner-equivalent access.** Anyone granted the configured `permissionId` through `JBPermissions` can satisfy owner checks for the scoped contract.
|
|
24
|
-
- **Deployment inputs are intentional.** If `initialProjectIdOwner != 0`, deployers must understand whether that project already exists.
|
|
25
|
-
|
|
26
|
-
## 2. Known Risks
|
|
27
|
-
|
|
28
|
-
- **Project NFT transfer changes contract ownership.** Anyone who acquires the project NFT gains owner access to contracts using that project-owned mode.
|
|
29
|
-
- **Two ownership modes can confuse integrations.** Setting both `newOwner` and `projectId` is disallowed, but integrators still need to check which mode is active.
|
|
30
|
-
- **`renounceOwnership` is final.** Once called, `owner()` resolves to `address(0)` and owner-gated functions stop working permanently unless a downstream contract adds its own recovery path.
|
|
31
|
-
- **Constructor pre-binding can intentionally lock the contract.** If a deployer points ownership at a future project ID, `owner()` resolves to `address(0)` until that project exists.
|
|
32
|
-
- **`PROJECTS == address(0)` breaks project-owned mode.** The constructor defends against this, but wrappers should still treat it as a high-signal deployment surface.
|
|
33
|
-
- **Unminted project ID ownership.** Contracts using `JBOwnableOverrides` can be configured with an `initialProjectIdOwner` that references a project ID not yet minted. The first account to mint that sequential project ID will become the effective owner of the contract. Deployers must ensure the referenced project ID is already minted, or deploy the ownable contract and the project in the same transaction to prevent front-running.
|
|
34
|
-
|
|
35
|
-
## 3. Accepted Behaviors
|
|
36
|
-
|
|
37
|
-
- **Permission ID resets on transfer.** `permissionId` resets to `0` on ownership transfer so old delegated operators do not automatically retain power.
|
|
38
|
-
- **`permissionId = 0` means direct-owner-only mode.** This is a valid configuration, not an error state.
|
|
39
|
-
- **Invalid project ownership resolves fail-closed.** If `ownerOf` cannot resolve, the contract is effectively renounced until ownership becomes readable again.
|
|
40
|
-
- **`transferOwnershipToProject` rejects non-existent projects.** The function checks existence at transfer time.
|
|
41
|
-
- **Constructor pre-binding to a future project ID is supported.** This is useful in controlled deployment flows, but dangerous if the deployer does not control mint sequencing.
|
|
42
|
-
- **Transfer events can temporarily show `address(0)`.** When ownership points to an unminted future project, the transfer event shows `address(0)` until ownership can resolve dynamically.
|
|
43
|
-
|
|
44
|
-
## 4. Invariants To Verify
|
|
45
|
-
|
|
46
|
-
- ownership is always exactly one of: direct address or project NFT holder
|
|
47
|
-
- `_checkOwner()` reverts for all callers when the owner resolves to `address(0)`
|
|
48
|
-
- `permissionId` resets to `0` on every ownership transfer
|
|
49
|
-
- after `renounceOwnership()`, `jbOwner()` returns `(address(0), 0, 0)` and no address can pass `_checkOwner()`
|
|
50
|
-
- `transferOwnershipToProject(projectId)` reverts for all `projectId > PROJECTS.count()` at call time
|
|
51
|
-
- `initialProjectIdOwner != 0` with `PROJECTS == address(0)` always reverts during construction
|
package/SKILLS.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Juicebox Ownable
|
|
2
|
-
|
|
3
|
-
## Use This File For
|
|
4
|
-
|
|
5
|
-
- Use this file when the task involves project-based ownership, delegated `onlyOwner` permissions, or ownership that should follow a Juicebox project NFT instead of a fixed wallet.
|
|
6
|
-
- Start here, then decide whether the question is about owner resolution, permission delegation, or ownership transfer semantics.
|
|
7
|
-
|
|
8
|
-
## Read This Next
|
|
9
|
-
|
|
10
|
-
| If you need... | Open this next |
|
|
11
|
-
|---|---|
|
|
12
|
-
| Repo overview and ownership model | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
|
|
13
|
-
| Concrete contract | [`src/JBOwnable.sol`](./src/JBOwnable.sol) |
|
|
14
|
-
| Resolution and permission logic | [`src/JBOwnableOverrides.sol`](./src/JBOwnableOverrides.sol), [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/) |
|
|
15
|
-
| Runtime and migration assumptions | [`references/runtime.md`](./references/runtime.md), [`references/operations.md`](./references/operations.md) |
|
|
16
|
-
| Edge, attack, and invariant coverage | [`test/Ownable.t.sol`](./test/Ownable.t.sol), [`test/OwnableEdgeCases.t.sol`](./test/OwnableEdgeCases.t.sol), [`test/OwnableAttacks.t.sol`](./test/OwnableAttacks.t.sol), [`test/OwnableInvariantTests.sol`](./test/OwnableInvariantTests.sol), [`test/CodexUnmintedProjectHijack.t.sol`](./test/CodexUnmintedProjectHijack.t.sol) |
|
|
17
|
-
|
|
18
|
-
## Repo Map
|
|
19
|
-
|
|
20
|
-
| Area | Where to look |
|
|
21
|
-
|---|---|
|
|
22
|
-
| Main contracts | [`src/`](./src/) |
|
|
23
|
-
| Types | [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/) |
|
|
24
|
-
| Tests | [`test/`](./test/) |
|
|
25
|
-
|
|
26
|
-
## Purpose
|
|
27
|
-
|
|
28
|
-
Ownership adapter for contracts that should follow Juicebox project ownership instead of a fixed address, with optional delegated permission IDs on top of the familiar `Ownable` pattern.
|
|
29
|
-
|
|
30
|
-
## Reference Files
|
|
31
|
-
|
|
32
|
-
- Open [`references/runtime.md`](./references/runtime.md) when you need owner resolution, transfer semantics, or delegated access behavior.
|
|
33
|
-
- Open [`references/operations.md`](./references/operations.md) when you need migration pitfalls, test breadcrumbs, or the common stale assumptions around permission resets.
|
|
34
|
-
|
|
35
|
-
## Working Rules
|
|
36
|
-
|
|
37
|
-
- Start in [`src/JBOwnableOverrides.sol`](./src/JBOwnableOverrides.sol) when the question is about who the effective owner is or why `onlyOwner` passed or failed.
|
|
38
|
-
- Treat ownership transfer and delegated permission resets as security-sensitive.
|
|
39
|
-
- Project-based ownership can intentionally become unusable if it points at an unminted or invalid project.
|
|
40
|
-
- Unminted or unexpectedly transferred project NFTs can change the effective owner surface. Check the project lifecycle, not just this adapter.
|
|
41
|
-
- When a bug looks like project ownership itself, confirm whether the real source is upstream in `nana-core-v6` rather than this adapter.
|