@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/README.md CHANGED
@@ -1,124 +1,104 @@
1
1
  # Juicebox Ownable
2
2
 
3
- Ownership that follows the project, not a person. Transfer control of any contract to a Juicebox project NFT, and anyone the project owner delegates through `JBPermissions` can act as owner.
3
+ `@bananapus/ownable-v6` is an ownership helper for contracts that should be controlled by a Juicebox project rather than a fixed wallet. It keeps the familiar `Ownable` shape while letting ownership follow a project NFT and optional delegated permissions.
4
4
 
5
- This is a variation on OpenZeppelin [`Ownable`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol) that adds:
5
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
+ User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
7
+ Skills: [SKILLS.md](./SKILLS.md)
8
+ Risks: [RISKS.md](./RISKS.md)
9
+ Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
10
+ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
6
11
 
7
- - The ability to transfer contract ownership to a Juicebox project instead of a specific address. When owned by a project, ownership dynamically follows whoever holds that project's ERC-721 NFT -- no on-chain update needed.
8
- - The ability to grant other addresses `onlyOwner` access using `JBPermissions`, with a configurable `permissionId`.
9
- - `JBPermissioned` base class with support for OpenZeppelin `Context` (enabling optional meta-transaction support).
12
+ ## Overview
10
13
 
11
- All features are backwards compatible with OpenZeppelin `Ownable`. `JBOwnable` is a drop-in replacement: it provides the same `onlyOwner` modifier, `owner()` view, `transferOwnership(address)`, and `renounceOwnership()` functions.
14
+ This package extends the standard ownership model in three useful ways:
12
15
 
13
- Forked from [`jbx-protocol/juice-ownable`](https://github.com/jbx-protocol/juice-ownable).
16
+ - ownership can point to a Juicebox project ID instead of an address
17
+ - `owner()` resolves dynamically to the current holder of that project NFT when the referenced project remains readable
18
+ - delegated operators can satisfy `onlyOwner` through a configurable `JBPermissions` permission ID
14
19
 
15
- _If you have questions, take a look at the [core protocol contracts](https://github.com/Bananapus/nana-core-v6) and the [documentation](https://docs.juicebox.money/) first, or reach out on [Discord](https://discord.com/invite/ErQYmth4dS)._
20
+ For contracts that are already conceptually "owned by the project," this avoids manual ownership transfers when the project NFT changes hands.
16
21
 
17
- ## Repository Layout
22
+ Use this repo when ownership should follow a Juicebox project. Do not use it if plain single-address ownership is good enough; standard `Ownable` is simpler.
18
23
 
19
- ```
20
- src/
21
- ├── JBOwnable.sol -- Concrete implementation (inherit this)
22
- ├── JBOwnableOverrides.sol -- Abstract base with all ownership logic
23
- ├── interfaces/
24
- │ └── IJBOwnable.sol -- Interface for ownership queries, transfers, and events
25
- └── structs/
26
- └── JBOwner.sol -- Packed struct: owner (160 bits) + projectId (88 bits) + permissionId (8 bits)
24
+ If your issue is in project ownership itself, start in `nana-core-v6` and `JBProjects`. This repo starts mattering when another contract wants its own admin surface to follow that project ownership.
27
25
 
28
- test/
29
- ├── Ownable.t.sol -- Core ownership tests
30
- ├── OwnableAttacks.t.sol -- Attack vector tests
31
- ├── OwnableEdgeCases.t.sol -- Edge case coverage
32
- ├── OwnableInvariantTests.sol -- Invariant/fuzz tests
33
- ├── handlers/
34
- │ └── OwnableHandler.sol -- Invariant test handler
35
- ├── mocks/
36
- │ └── MockOwnable.sol -- Mock contract for testing
37
- └── regression/
38
- ├── BurnLockProtection.t.sol -- Regression: burned NFT lockout
39
- └── ZeroAddressValidation.t.sol -- Regression: zero-address edge cases
40
- ```
26
+ ## Key Contracts
41
27
 
42
- ## Architecture
28
+ | Contract | Role |
29
+ | --- | --- |
30
+ | `JBOwnable` | Concrete contract to inherit when you want Juicebox-aware ownership with the standard `onlyOwner` interface. |
31
+ | `JBOwnableOverrides` | Abstract base that holds the owner-resolution and permission-checking logic. |
32
+ | `IJBOwnable` | Interface for queries, transfers, permission ID changes, and events. |
43
33
 
44
- ```
45
- JBOwnable
46
- └── JBOwnableOverrides (abstract)
47
- ├── Context (OpenZeppelin)
48
- ├── JBPermissioned (nana-core-v6)
49
- └── IJBOwnable (interface)
50
- ```
51
-
52
- | Contract | Description |
53
- |----------|-------------|
54
- | [`JBOwnable`](src/JBOwnable.sol) | Concrete implementation. Provides the `onlyOwner` modifier and emits `OwnershipTransferred` events (resolving project NFT holders at emission time). Inherit this in your contract. |
55
- | [`JBOwnableOverrides`](src/JBOwnableOverrides.sol) | Abstract base containing all ownership logic: owner resolution, transfers, renunciation, permission delegation, and internal helpers. Use this directly only if you need to customize `_emitTransferEvent` (e.g., when deploying contracts before the project NFT is minted). |
34
+ ## Mental Model
56
35
 
57
- ### Supporting Types
36
+ This package is a thin ownership adapter:
58
37
 
59
- | Type | Location | Description |
60
- |------|----------|-------------|
61
- | [`JBOwner`](src/structs/JBOwner.sol) | `src/structs/` | Struct packing `address owner` (160 bits), `uint88 projectId` (88 bits), and `uint8 permissionId` (8 bits) into a single 256-bit storage slot. |
62
- | [`IJBOwnable`](src/interfaces/IJBOwnable.sol) | `src/interfaces/` | Interface exposing ownership queries, transfers, renunciation, permission ID management, and events. |
38
+ 1. resolve who the effective owner is
39
+ 2. optionally delegate `onlyOwner` through a permission ID
40
+ 3. preserve an `Ownable`-like interface for downstream contracts
63
41
 
64
- ### Ownership Modes
42
+ ## Read These Files First
65
43
 
66
- 1. **Project ownership** -- If `JBOwner.projectId` is nonzero, the current holder of that `JBProjects` ERC-721 is the owner. Ownership automatically follows the NFT: when the NFT is transferred, `owner()` immediately reflects the new holder without any additional transaction.
67
- 2. **Address ownership** -- If `projectId` is zero, `JBOwner.owner` is the owner directly.
68
- 3. **Delegated access** -- The owner can grant other addresses access via `JBPermissions.setPermissionsFor(...)` using the configured `permissionId`. The owner must first call `setPermissionId(uint8)` to set which permission ID represents owner-level access.
69
- 4. **Renounced** -- After calling `renounceOwnership()`, both `owner` and `projectId` are set to zero. No one can call `onlyOwner` functions. This is irreversible.
44
+ 1. `src/JBOwnable.sol`
45
+ 2. `src/JBOwnableOverrides.sol`
46
+ 3. `src/interfaces/IJBOwnable.sol`
70
47
 
71
- The `permissionId` resets to 0 on every ownership transfer to prevent permission clashes for the new owner.
48
+ ## Integration Traps
72
49
 
73
- ### Events
50
+ - ownership may resolve to a project NFT holder rather than a fixed address, so caching `owner()` off-chain can become stale
51
+ - `owner()` can resolve to `address(0)` if the referenced project NFT is burned, invalid, or otherwise unreadable, which effectively renounces the contract
52
+ - delegated operator access depends on a chosen permission ID, not on a generic admin role
53
+ - ownership transfer and permission-ID updates are part of the security model, not just convenience helpers
74
54
 
75
- | Event | Emitted By |
76
- |-------|-----------|
77
- | `OwnershipTransferred(address indexed previousOwner, address indexed newOwner, address caller)` | `_emitTransferEvent` (called on every ownership change) |
78
- | `PermissionIdChanged(uint8 newId, address caller)` | `_setPermissionId` |
55
+ ## Where State Lives
79
56
 
80
- ### Errors
57
+ - effective ownership configuration lives in `JBOwnableOverrides`
58
+ - downstream contract state still lives in the inheriting contract, not in this package
59
+ - project ownership truth lives in `nana-core-v6` when the owner target is a Juicebox project
81
60
 
82
- | Error | Thrown When |
83
- |-------|-----------|
84
- | `JBOwnableOverrides_InvalidNewOwner()` | Constructor receives both zero owner and zero projectId; `transferOwnership` receives zero address; `transferOwnershipToProject` receives zero or overflow projectId; `_transferOwnership` receives both non-zero owner and non-zero projectId. |
85
- | `JBOwnableOverrides_ProjectDoesNotExist()` | `transferOwnershipToProject` receives a projectId greater than `PROJECTS.count()`. |
86
- | `JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner()` | Constructor receives a non-zero `initialProjectIdOwner` with `projects` set to `address(0)`. |
61
+ ## High-Signal Tests
87
62
 
88
- ## How Ownership Resolution Works
63
+ 1. `test/Ownable.t.sol`
64
+ 2. `test/OwnableAttacks.t.sol`
65
+ 3. `test/CodexUnmintedProjectHijack.t.sol`
66
+ 4. `test/regression/BurnLockProtection.t.sol`
89
67
 
90
- When `_checkOwner()` is called (by the `onlyOwner` modifier), it:
68
+ ## Install
91
69
 
92
- 1. Reads the `JBOwner` struct from storage.
93
- 2. Resolves the owner address: if `projectId != 0`, calls `PROJECTS.ownerOf(projectId)` via try-catch (returns `address(0)` if the call reverts, e.g., burned NFT); otherwise uses the stored `owner` address.
94
- 3. Calls `_requirePermissionFrom(account, projectId, permissionId)` from `JBPermissioned`, which passes if the caller is:
95
- - The resolved owner address, OR
96
- - An address granted the configured `permissionId` on the relevant project via `JBPermissions`, OR
97
- - An address granted the ROOT permission (permission ID 1) on the relevant project via `JBPermissions`.
70
+ ```bash
71
+ npm install @bananapus/ownable-v6
72
+ ```
98
73
 
99
- ## How Delegated Access Works
74
+ ## Development
100
75
 
101
- 1. The owner calls `setPermissionId(uint8)` on the `JBOwnable` contract to configure which permission ID represents owner-level access.
102
- 2. The owner calls `JBPermissions.setPermissionsFor(...)` to grant that permission ID to specific addresses on the relevant project.
103
- 3. Those addresses can now call `onlyOwner` functions.
104
- 4. If the project NFT is transferred to a new holder, delegated permissions granted by the previous holder stop working -- the new holder must re-grant permissions.
76
+ ```bash
77
+ npm install
78
+ forge build
79
+ forge test
80
+ ```
105
81
 
106
- ## Risks
82
+ ## Repository Layout
107
83
 
108
- - **Burned project NFT permanently locks the contract.** If ownership is tied to a project and the project NFT is burned, `owner()` returns `address(0)` (the `ownerOf` call reverts and the try-catch falls through). No one can call `onlyOwner` functions, and there is no recovery path -- this is effectively permanent renunciation. JBProjects V6 does not expose a public burn function, but a project NFT could still be burned via a custom token wrapper or a future upgrade.
109
- - **Silent loss of delegated access on ownership transfer.** The `permissionId` resets to 0 on every ownership transfer (both `transferOwnership` and `transferOwnershipToProject`). Any operators previously granted the old `permissionId` via `JBPermissions` lose their `onlyOwner` access without notification. The new owner must call `setPermissionId` and re-grant permissions to restore delegated access. Off-chain monitoring of the `PermissionIdChanged` event is recommended.
110
- - **Permission ID collisions with other JBPermissions grants.** The `permissionId` set on a `JBOwnable` contract is checked via the same `JBPermissions` registry used by the rest of the protocol. If the chosen `permissionId` overlaps with an ID already granted to operators for other purposes (e.g., a terminal permission or a hook permission), those operators will also pass the `onlyOwner` check -- gaining unintended owner-level access to the `JBOwnable` contract. Choose a `permissionId` that is not used by any other permission in the project's permission set.
84
+ ```text
85
+ src/
86
+ JBOwnable.sol
87
+ JBOwnableOverrides.sol
88
+ interfaces/
89
+ structs/
90
+ test/
91
+ core, attack, invariant, mock, and regression coverage
92
+ ```
111
93
 
112
- ## Install
94
+ ## Risks And Notes
113
95
 
114
- ```bash
115
- npm install
116
- ```
96
+ - if ownership is tied to a project NFT and that NFT becomes unreachable, the contract is effectively locked
97
+ - delegated access depends on a chosen permission ID, so collisions with other permission schemes are an operational risk
98
+ - permission IDs reset on ownership transfer, which is safer by default but easy to miss if an integration expects long-lived operator access
99
+ - transferring ownership to a project validates that the project exists, but later project-NFT invalidation can still collapse effective ownership to `address(0)`
117
100
 
118
- ## Develop
101
+ ## For AI Agents
119
102
 
120
- | Command | Description |
121
- |---------|-------------|
122
- | `forge build` | Compile contracts |
123
- | `forge test` | Run tests |
124
- | `forge coverage --match-path "./src/*.sol" --report lcov --report summary` | Generate coverage report |
103
+ - Do not collapse project-based ownership into ordinary wallet-based ownership in your summary.
104
+ - Read the attack and regression tests before making claims about burn-lock or unminted-project edge cases.
package/RISKS.md CHANGED
@@ -1,27 +1,51 @@
1
- # RISKS.md -- nana-ownable-v6
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. |
2
18
 
3
19
  ## 1. Trust Assumptions
4
20
 
5
- - **JBPermissions.** Permission checks delegate to JBPermissions contract. A bug in JBPermissions affects all JBOwnable contracts.
6
- - **JBProjects ERC-721.** When owned by a project, ownership follows the ERC-721 token. Whoever holds the project NFT has owner access.
7
- - **Permission Delegation.** Anyone granted the configured `permissionId` via JBPermissions gets owner-equivalent access for the scoped function.
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.
8
25
 
9
26
  ## 2. Known Risks
10
27
 
11
- - **Project NFT transfer = ownership transfer.** If ownership is tied to a project ID, anyone who acquires the project NFT (via transfer, marketplace purchase, or social engineering) gains full owner access to all contracts using that JBOwnable instance. Project NFT holders must treat the NFT as a high-value key.
12
- - **Dual ownership ambiguity.** Setting both `newOwner` and `projectId` to non-zero reverts, but the two-mode design could confuse integrators about which mode is active. `jbOwner()` exposes both fields for inspection.
13
- - **`renounceOwnership` permanently disables all owner-gated functions.** Defined directly in `JBOwnableOverrides` (not inherited from OpenZeppelin's `Ownable` -- the contract does not inherit from `Ownable`). Once called, `owner()` returns `address(0)` and all `onlyOwner` / `_checkOwner()` calls revert permanently. There is no recovery mechanism. This applies whether ownership is direct (address) or project-based (project NFT holder).
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.
14
34
 
15
35
  ## 3. Accepted Behaviors
16
36
 
17
- - **Permission ID reset on transfer.** `permissionId` resets to 0 on ownership transfer, which temporarily locks out delegated operators. This is intentional -- it prevents permission clashes for new owners.
18
- - **Burned/invalid project NFT.** If the project NFT were burned or `ownerOf` reverted, the contract would be effectively renounced (owner resolves to `address(0)`). Defensive try-catch in `owner()` and `_checkOwner()` handles this gracefully. JBProjects V6 has no burn function, so this scenario cannot occur in practice.
19
- - **`transferOwnershipToProject` rejects non-existent projects.** The function checks `projectId > PROJECTS.count()` and reverts, preventing transfers to non-existent projects.
20
-
21
- ## 4. Invariants to Verify
22
-
23
- - Ownership is always exactly one of: direct address OR project NFT holder (never both, never neither unless renounced).
24
- - `_checkOwner()` reverts for all callers when the owner resolves to `address(0)`.
25
- - `permissionId` is correctly reset to 0 on every ownership transfer.
26
- - After `renounceOwnership()`, `jbOwner()` returns `(address(0), 0, 0)` and no address can pass `_checkOwner()`.
27
- - `transferOwnershipToProject(projectId)` reverts for all `projectId > PROJECTS.count()` at call time.
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 CHANGED
@@ -1,185 +1,41 @@
1
1
  # Juicebox Ownable
2
2
 
3
- ## Purpose
4
-
5
- Drop-in Juicebox-aware replacement for OpenZeppelin `Ownable`. A contract inheriting `JBOwnable` can be owned by a Juicebox project (via its ERC-721 NFT) or a plain address, with delegated access through `JBPermissions`. When owned by a project, ownership dynamically follows the NFT holder -- no on-chain update is needed when the NFT changes hands.
6
-
7
- ## Contracts
8
-
9
- | Contract | Role |
10
- |----------|------|
11
- | `JBOwnable` | Concrete implementation. Provides the `onlyOwner` modifier and `_emitTransferEvent` (resolves project NFT holder at emission time). Inherit this in your contract. |
12
- | `JBOwnableOverrides` | Abstract base with all ownership state and logic: owner resolution, transfers, renunciation, permission delegation, and internal helpers. Only inherit this directly if you need to customize `_emitTransferEvent`. |
13
-
14
- ## Inheritance Chain
15
-
16
- ```
17
- JBOwnable
18
- └── JBOwnableOverrides (abstract)
19
- ├── Context (@openzeppelin/contracts)
20
- ├── JBPermissioned (@bananapus/core-v6)
21
- └── IJBOwnable (interface)
22
- ```
23
-
24
- ## Deployed Addresses
25
-
26
- Library contract -- deployed as part of inheriting contracts (e.g., `JB721TiersHook`, `JBBuybackHook`). No standalone deployment.
27
-
28
- ## Key Functions
29
-
30
- ### Public / External
31
-
32
- | Function | Contract | What it does |
33
- |----------|----------|--------------|
34
- | `owner()` | `JBOwnableOverrides` | Returns the current owner address. If `projectId != 0`, calls `PROJECTS.ownerOf(projectId)` via try-catch -- returns the project NFT holder on success, or `address(0)` if the call reverts (e.g., burned NFT). Otherwise returns `jbOwner.owner`. |
35
- | `transferOwnership(address newOwner)` | `JBOwnableOverrides` | Transfers ownership to a new address. Reverts if `newOwner` is `address(0)`. Resets `permissionId` to 0. |
36
- | `transferOwnershipToProject(uint256 projectId)` | `JBOwnableOverrides` | Transfers ownership to a Juicebox project. The NFT holder becomes the owner. Validates: `projectId != 0`, fits in `uint88`, and `projectId <= PROJECTS.count()`. Resets `permissionId` to 0. |
37
- | `renounceOwnership()` | `JBOwnableOverrides` | Permanently gives up ownership. Sets both `owner` and `projectId` to zero. Irreversible -- no one can call `onlyOwner` functions afterward. |
38
- | `setPermissionId(uint8 permissionId)` | `JBOwnableOverrides` | Sets which `JBPermissions` permission ID grants delegated owner access. Only callable by the current owner. |
39
-
40
- ### Internal
41
-
42
- | Function | Contract | What it does |
43
- |----------|----------|--------------|
44
- | `_checkOwner()` | `JBOwnableOverrides` | Resolves the owner, then calls `_requirePermissionFrom(account, projectId, permissionId)`. Used by the `onlyOwner` modifier. |
45
- | `_transferOwnership(address newOwner, uint88 projectId)` | `JBOwnableOverrides` | Core transfer logic. Reverts if both `newOwner` and `projectId` are non-zero. Updates `jbOwner` struct and resets `permissionId` to 0. Calls `_emitTransferEvent`. No access restriction. |
46
- | `_transferOwnership(address newOwner)` | `JBOwnableOverrides` | Convenience overload that calls `_transferOwnership(newOwner, 0)`. Exists for drop-in compatibility with OpenZeppelin `Ownable`. |
47
- | `_setPermissionId(uint8 permissionId)` | `JBOwnableOverrides` | Sets `jbOwner.permissionId` and emits `PermissionIdChanged`. No access restriction -- meant for internal use. |
48
- | `_emitTransferEvent(address previousOwner, address newOwner, uint88 newProjectId)` | `JBOwnable` (overrides abstract in `JBOwnableOverrides`) | Emits `OwnershipTransferred`. Resolves `newProjectId` to the current NFT holder via `PROJECTS.ownerOf()`. Override this in `JBOwnableOverrides` subclasses if you need custom transfer event behavior (e.g., deploying before a project NFT is minted). |
49
-
50
- ## Immutable State
51
-
52
- | Variable | Type | Description |
53
- |----------|------|-------------|
54
- | `PROJECTS` | `IJBProjects` | The `JBProjects` ERC-721 contract used to resolve project ownership. |
55
- | `PERMISSIONS` | `IJBPermissions` | Inherited from `JBPermissioned`. The `JBPermissions` contract used for delegated access checks. |
56
-
57
- ## Key Types
3
+ ## Use This File For
58
4
 
59
- | Type | Fields | Storage |
60
- |------|--------|---------|
61
- | `JBOwner` | `address owner` (160 bits), `uint88 projectId` (88 bits), `uint8 permissionId` (8 bits) | Single 256-bit slot. `owner` and `projectId` are mutually exclusive (both non-zero is invalid). |
5
+ - Use this file when the task involves project-based ownership, delegated `onlyOwner` permissions, or how ownership 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. Those surfaces are intentionally compact but security-sensitive.
62
7
 
63
- ## Events
8
+ ## Read This Next
64
9
 
65
- | Event | Fields | When |
66
- |-------|--------|------|
67
- | `OwnershipTransferred` | `address indexed previousOwner`, `address indexed newOwner`, `address caller` | Every ownership change (transfer, project transfer, renounce). |
68
- | `PermissionIdChanged` | `uint8 newId`, `address caller` | When `setPermissionId` or `_setPermissionId` is called. |
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) |
69
17
 
70
- ## Errors
18
+ ## Repo Map
71
19
 
72
- | Error | When |
73
- |-------|------|
74
- | `JBOwnableOverrides_InvalidNewOwner()` | Constructor gets both zero owner and zero projectId. `transferOwnership(address(0))`. `transferOwnershipToProject(0)` or `transferOwnershipToProject(id > type(uint88).max)`. `_transferOwnership` called with both non-zero owner and non-zero projectId. |
75
- | `JBOwnableOverrides_ProjectDoesNotExist()` | `transferOwnershipToProject(id)` where `id > PROJECTS.count()`. |
76
- | `JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner()` | Constructor receives a non-zero `initialProjectIdOwner` with `projects` set to `address(0)`. |
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/) |
77
25
 
78
- ## IJBOwnable Interface
79
-
80
- Defined in `src/interfaces/IJBOwnable.sol`. Pragma `^0.8.0`.
81
-
82
- | Method | Signature | Returns |
83
- |--------|-----------|---------|
84
- | `PROJECTS()` | `function PROJECTS() external view` | `IJBProjects` |
85
- | `jbOwner()` | `function jbOwner() external view` | `(address owner, uint88 projectId, uint8 permissionId)` |
86
- | `owner()` | `function owner() external view` | `address` |
87
- | `renounceOwnership()` | `function renounceOwnership() external` | -- |
88
- | `setPermissionId(uint8)` | `function setPermissionId(uint8 permissionId) external` | -- |
89
- | `transferOwnership(address)` | `function transferOwnership(address newOwner) external` | -- |
90
- | `transferOwnershipToProject(uint256)` | `function transferOwnershipToProject(uint256 projectId) external` | -- |
91
-
92
- Events: `OwnershipTransferred(address indexed previousOwner, address indexed newOwner, address caller)`, `PermissionIdChanged(uint8 newId, address caller)`.
93
-
94
- ## Integration Points
95
-
96
- | Dependency | Import | Used For |
97
- |------------|--------|----------|
98
- | `nana-core-v6` | `IJBPermissions`, `JBPermissioned` | Permission checks for delegated access via `_requirePermissionFrom` |
99
- | `nana-core-v6` | `IJBProjects` | Resolving project NFT holder as owner via `PROJECTS.ownerOf(projectId)` |
100
- | `@openzeppelin/contracts` | `Context` | `_msgSender()` support for meta-transaction compatibility |
101
-
102
- ## Gotchas
103
-
104
- - **Mutually exclusive ownership modes.** You cannot set both `owner` and `projectId` to non-zero values. The constructor, `transferOwnership`, and `transferOwnershipToProject` all enforce this. `_transferOwnership` reverts with `JBOwnableOverrides_InvalidNewOwner` if both are non-zero.
105
- - **Constructor rejects zero-zero initialization.** If both `initialOwner` and `initialProjectIdOwner` are zero, the constructor reverts. To create an unowned contract, set an initial owner and call `renounceOwnership()` in the constructor body.
106
- - **`permissionId` resets to 0 on every ownership transfer.** This is intentional -- it prevents stale permission delegation from carrying over to new owners. The new owner must explicitly call `setPermissionId()` to re-enable delegated access.
107
- - **Delegated permissions are scoped to the granting address.** When a project NFT is transferred, delegates authorized by the old holder lose access immediately. The new holder must re-grant permissions via `JBPermissions.setPermissionsFor(...)`.
108
- - **ROOT permission (ID 1) always grants access.** `_checkOwner()` delegates to `_requirePermissionFrom` from `JBPermissioned`, which recognizes the ROOT permission (ID 1) as granting access regardless of the configured `permissionId`.
109
- - **`renounceOwnership()` is irreversible.** After renouncing, `owner()` returns `address(0)`, and all `onlyOwner` functions permanently revert. There is no recovery mechanism.
110
- - **`projectId` is `uint88`.** Project IDs above `type(uint88).max` (309,485,009,821,345,068,724,781,055) are rejected by `transferOwnershipToProject`. This constraint enables the `JBOwner` struct to fit in a single storage slot.
111
- - **`transferOwnershipToProject` checks existence.** It compares the project ID against `PROJECTS.count()` and reverts with `JBOwnableOverrides_ProjectDoesNotExist` if the project does not exist, preventing permanent loss of contract control.
112
- - **`owner()` makes an external call in project mode.** When `projectId != 0`, `owner()` calls `PROJECTS.ownerOf(projectId)`, which is an external call. This is relevant for gas-sensitive contexts. If the call reverts (e.g., the project NFT was burned), `owner()` returns `address(0)` and the contract degrades to a renounced state.
113
- - **Constructor rejects zero-address PROJECTS with project ownership.** Deploying with `projects = address(0)` and a non-zero `initialProjectIdOwner` reverts with `JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner`, preventing permanently broken ownership resolution.
114
- - **No ERC2771 support.** Despite inheriting `Context`, `JBOwnable` uses plain `Context._msgSender()` (which returns `msg.sender`), not `ERC2771Context`. A trusted forwarder appending a sender address to calldata has no effect on ownership checks. If you need meta-tx support, override `_msgSender()` and `_msgData()` with `ERC2771Context` in your inheriting contract.
115
-
116
- ## Example: Inherit JBOwnable
117
-
118
- ```solidity
119
- import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
120
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
121
- import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
122
-
123
- contract MyHook is JBOwnable {
124
- constructor(
125
- IJBPermissions permissions,
126
- IJBProjects projects,
127
- uint88 projectId
128
- ) JBOwnable(permissions, projects, address(0), projectId) {}
129
-
130
- function restrictedAction() external onlyOwner {
131
- // Only the project NFT holder (or delegated addresses) can call this.
132
- }
133
- }
134
- ```
135
-
136
- ## Example: Address-Owned Contract
137
-
138
- ```solidity
139
- contract MyContract is JBOwnable {
140
- constructor(
141
- IJBPermissions permissions,
142
- IJBProjects projects,
143
- address initialOwner
144
- ) JBOwnable(permissions, projects, initialOwner, 0) {}
145
-
146
- function adminFunction() external onlyOwner {
147
- // Only initialOwner (or delegated addresses) can call this.
148
- }
149
- }
150
- ```
151
-
152
- ## Example: Override _emitTransferEvent
26
+ ## Purpose
153
27
 
154
- If you need to deploy a `JBOwnableOverrides`-based contract before the project NFT exists (e.g., the contract is deployed as part of the project creation flow), override `_emitTransferEvent` to handle the case where the project ID is not yet minted:
28
+ Ownership adapter for contracts that should follow Juicebox project ownership instead of a fixed address, with optional delegated permission IDs layered on top of the familiar `Ownable` pattern.
155
29
 
156
- ```solidity
157
- import {JBOwnableOverrides} from "@bananapus/ownable-v6/src/JBOwnableOverrides.sol";
30
+ ## Reference Files
158
31
 
159
- contract MyPreDeployContract is JBOwnableOverrides {
160
- modifier onlyOwner() {
161
- _checkOwner();
162
- _;
163
- }
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.
164
34
 
165
- constructor(
166
- IJBPermissions permissions,
167
- IJBProjects projects,
168
- address initialOwner,
169
- uint88 initialProjectIdOwner
170
- ) JBOwnableOverrides(permissions, projects, initialOwner, initialProjectIdOwner) {}
35
+ ## Working Rules
171
36
 
172
- function _emitTransferEvent(
173
- address previousOwner,
174
- address newOwner,
175
- uint88 newProjectId
176
- ) internal override {
177
- // Custom logic -- e.g., skip ownerOf() if the project NFT is not yet minted.
178
- emit OwnershipTransferred({
179
- previousOwner: previousOwner,
180
- newOwner: newProjectId == 0 ? newOwner : address(0), // Resolve later.
181
- caller: msg.sender
182
- });
183
- }
184
- }
185
- ```
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. Treat that as a deployment invariant, not a runtime surprise.
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 layer.