@bananapus/ownable-v6 0.0.17 → 0.0.18

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,22 +1,45 @@
1
- # RISKS.md -- nana-ownable-v6
1
+ # Juicebox Ownable Risk Register
2
+
3
+ This file focuses on the ownership-model risks in `JBOwnable`: dynamic ownership through project NFTs, delegated owner authority, and the mismatch between EOA-style expectations and Juicebox project control.
4
+
5
+ ## How to use this file
6
+
7
+ - Read `Priority risks` first; most failures here are authority-model misunderstandings rather than arithmetic bugs.
8
+ - Use the detailed sections to understand what changes when ownership follows a project instead of a fixed address.
9
+ - Treat `Invariants to Verify` as the minimal 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, surprising integrators that expect static `Ownable` semantics. | Clear docs, careful integration review, and explicit tests around ownership transfer paths. |
16
+ | P1 | Over-broad delegated owner permissions | `JBPermissions` can broaden who effectively acts as owner; sloppy configuration expands blast radius fast. | Permission hygiene and explicit review of delegated owner grants. |
17
+ | P2 | Interoperability assumptions with standard `Ownable` tooling | Some tooling assumes `owner()` maps to one address with no external permission system behind it. | Integration testing with downstream tooling and documentation of semantic differences. |
18
+
2
19
 
3
20
  ## 1. Trust Assumptions
4
21
 
5
22
  - **JBPermissions.** Permission checks delegate to JBPermissions contract. A bug in JBPermissions affects all JBOwnable contracts.
6
23
  - **JBProjects ERC-721.** When owned by a project, ownership follows the ERC-721 token. Whoever holds the project NFT has owner access.
7
24
  - **Permission Delegation.** Anyone granted the configured `permissionId` via JBPermissions gets owner-equivalent access for the scoped function.
25
+ - **Deployment inputs.** If `initialProjectIdOwner != 0`, the constructor assumes `PROJECTS` is non-zero and that deployers understand whether that project already exists yet.
8
26
 
9
27
  ## 2. Known Risks
10
28
 
11
29
  - **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
30
  - **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
31
  - **`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).
32
+ - **Constructor pre-binding can intentionally lock the contract.** If a deployer sets `initialProjectIdOwner` to a future project ID, `owner()` resolves to `address(0)` until that project NFT exists. This is a supported deployment pattern, but it means the contract can appear renounced during the gap.
33
+ - **`PROJECTS == address(0)` is fatal for project-owned mode.** The constructor defends against this with `JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner`, but wrappers that abstract deployment should still treat project-owned initialization as a high-signal configuration surface.
14
34
 
15
35
  ## 3. Accepted Behaviors
16
36
 
17
37
  - **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.
38
+ - **`permissionId = 0` means direct-owner-only mode.** `setPermissionId(0)` is valid and leaves owner access resting only with the resolved owner address or project owner. This is not an error state; it is the explicit way to disable delegated owner operators.
18
39
  - **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
40
  - **`transferOwnershipToProject` rejects non-existent projects.** The function checks `projectId > PROJECTS.count()` and reverts, preventing transfers to non-existent projects.
41
+ - **Constructor pre-binding to a future project ID.** The constructor can intentionally store a project-based owner for a not-yet-minted project ID. This is accepted for deployment flows that also control project-ID reservation or mint sequencing. If integrators do not control who mints that future project first, the first minter of the project NFT becomes owner of the contract.
42
+ - **Transfer events can temporarily show `address(0)` as the new owner.** When ownership is transferred to an unminted future project ID, `_emitTransferEvent` intentionally emits `address(0)` until the project NFT exists and ownership can resolve dynamically.
20
43
 
21
44
  ## 4. Invariants to Verify
22
45
 
@@ -25,3 +48,4 @@
25
48
  - `permissionId` is correctly reset to 0 on every ownership transfer.
26
49
  - After `renounceOwnership()`, `jbOwner()` returns `(address(0), 0, 0)` and no address can pass `_checkOwner()`.
27
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.
package/STYLE_GUIDE.md CHANGED
@@ -21,13 +21,13 @@ One contract/interface/struct/enum per file. Name the file after the type it con
21
21
 
22
22
  ```solidity
23
23
  // Contracts — pin to exact version
24
- pragma solidity ^0.8.26;
24
+ pragma solidity 0.8.28;
25
25
 
26
26
  // Interfaces, structs, enums — caret for forward compatibility
27
27
  pragma solidity ^0.8.0;
28
28
 
29
- // Libraries — caret, may use newer features
30
- pragma solidity ^0.8.17;
29
+ // Libraries — pin to exact version like contracts
30
+ pragma solidity 0.8.28;
31
31
  ```
32
32
 
33
33
  ## Imports
@@ -86,12 +86,20 @@ contract JBExample is JBPermissioned, IJBExample {
86
86
 
87
87
  uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
88
88
 
89
+ //*********************************************************************//
90
+ // ------------------------ private constants ------------------------ //
91
+ //*********************************************************************//
92
+
89
93
  //*********************************************************************//
90
94
  // --------------- public immutable stored properties ---------------- //
91
95
  //*********************************************************************//
92
96
 
93
97
  IJBDirectory public immutable override DIRECTORY;
94
98
 
99
+ //*********************************************************************//
100
+ // -------------- internal immutable stored properties -------------- //
101
+ //*********************************************************************//
102
+
95
103
  //*********************************************************************//
96
104
  // --------------------- public stored properties -------------------- //
97
105
  //*********************************************************************//
@@ -100,10 +108,26 @@ contract JBExample is JBPermissioned, IJBExample {
100
108
  // -------------------- internal stored properties ------------------- //
101
109
  //*********************************************************************//
102
110
 
111
+ //*********************************************************************//
112
+ // -------------------- private stored properties -------------------- //
113
+ //*********************************************************************//
114
+
115
+ //*********************************************************************//
116
+ // ------------------- transient stored properties ------------------- //
117
+ //*********************************************************************//
118
+
103
119
  //*********************************************************************//
104
120
  // -------------------------- constructor ---------------------------- //
105
121
  //*********************************************************************//
106
122
 
123
+ //*********************************************************************//
124
+ // ---------------------------- modifiers ---------------------------- //
125
+ //*********************************************************************//
126
+
127
+ //*********************************************************************//
128
+ // ------------------------- receive / fallback ---------------------- //
129
+ //*********************************************************************//
130
+
107
131
  //*********************************************************************//
108
132
  // ---------------------- external transactions ---------------------- //
109
133
  //*********************************************************************//
@@ -112,10 +136,18 @@ contract JBExample is JBPermissioned, IJBExample {
112
136
  // ----------------------- external views ---------------------------- //
113
137
  //*********************************************************************//
114
138
 
139
+ //*********************************************************************//
140
+ // -------------------------- public views --------------------------- //
141
+ //*********************************************************************//
142
+
115
143
  //*********************************************************************//
116
144
  // ----------------------- public transactions ----------------------- //
117
145
  //*********************************************************************//
118
146
 
147
+ //*********************************************************************//
148
+ // ---------------------- internal transactions ---------------------- //
149
+ //*********************************************************************//
150
+
119
151
  //*********************************************************************//
120
152
  // ----------------------- internal helpers -------------------------- //
121
153
  //*********************************************************************//
@@ -134,17 +166,28 @@ contract JBExample is JBPermissioned, IJBExample {
134
166
  1. Custom errors
135
167
  2. Public constants
136
168
  3. Internal constants
137
- 4. Public immutable stored properties
138
- 5. Internal immutable stored properties
139
- 6. Public stored properties
140
- 7. Internal stored properties
141
- 8. Constructor
142
- 9. External transactions
143
- 10. External views
144
- 11. Public transactions
145
- 12. Internal helpers
146
- 13. Internal views
147
- 14. Private helpers
169
+ 4. Private constants
170
+ 5. Public immutable stored properties
171
+ 6. Internal immutable stored properties
172
+ 7. Public stored properties
173
+ 8. Internal stored properties
174
+ 9. Private stored properties
175
+ 10. Transient stored properties
176
+ 11. Constructor
177
+ 12. Modifiers
178
+ 13. Receive / fallback
179
+ 14. External transactions
180
+ 15. External views
181
+ 16. Public views
182
+ 17. Public transactions
183
+ 18. Internal transactions
184
+ 19. Internal helpers
185
+ 20. Internal views
186
+ 21. Private helpers
187
+
188
+ Use these additional section labels where they better match the contents of the block:
189
+ - `internal functions` is accepted as equivalent to `internal helpers`
190
+ - `events` and `structs` are acceptable in specialized contracts that define them explicitly
148
191
 
149
192
  Functions are alphabetized within each section.
150
193
 
@@ -326,7 +369,7 @@ Standard config across all repos:
326
369
 
327
370
  ```toml
328
371
  [profile.default]
329
- solc = '0.8.26'
372
+ solc = '0.8.28'
330
373
  evm_version = 'cancun'
331
374
  optimizer_runs = 200
332
375
  libs = ["node_modules", "lib"]
@@ -565,8 +608,3 @@ CI checks formatting via `forge fmt --check`.
565
608
  ### Contract Size Checks
566
609
 
567
610
  CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
568
-
569
-
570
- ## Repo-Specific Deviations
571
-
572
- None. This repo follows the standard configuration exactly.