@bananapus/ownable-v6 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +55 -71
- package/ARCHITECTURE.md +75 -48
- package/AUDIT_INSTRUCTIONS.md +46 -165
- package/CHANGELOG.md +39 -0
- package/README.md +72 -92
- package/RISKS.md +42 -18
- package/SKILLS.md +28 -172
- package/STYLE_GUIDE.md +58 -20
- package/USER_JOURNEYS.md +81 -162
- package/foundry.toml +2 -0
- package/package.json +3 -3
- package/references/operations.md +14 -0
- package/references/runtime.md +19 -0
- package/src/JBOwnableOverrides.sol +4 -0
- package/src/structs/JBOwner.sol +0 -1
- package/test/CodexUnmintedProjectHijack.t.sol +45 -0
- package/CHANGE_LOG.md +0 -235
package/README.md
CHANGED
|
@@ -1,124 +1,104 @@
|
|
|
1
1
|
# Juicebox Ownable
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
This package extends the standard ownership model in three useful ways:
|
|
12
15
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
This package is a thin ownership adapter:
|
|
58
37
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
42
|
+
## Read These Files First
|
|
65
43
|
|
|
66
|
-
1.
|
|
67
|
-
2.
|
|
68
|
-
3.
|
|
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
|
-
|
|
48
|
+
## Integration Traps
|
|
72
49
|
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
## Install
|
|
91
69
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
##
|
|
74
|
+
## Development
|
|
100
75
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
76
|
+
```bash
|
|
77
|
+
npm install
|
|
78
|
+
forge build
|
|
79
|
+
forge test
|
|
80
|
+
```
|
|
105
81
|
|
|
106
|
-
##
|
|
82
|
+
## Repository Layout
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
##
|
|
94
|
+
## Risks And Notes
|
|
113
95
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
##
|
|
101
|
+
## For AI Agents
|
|
119
102
|
|
|
120
|
-
|
|
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
|
-
#
|
|
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
|
-
-
|
|
6
|
-
-
|
|
7
|
-
- **
|
|
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
|
|
12
|
-
- **
|
|
13
|
-
- **`renounceOwnership`
|
|
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
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
- `
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
8
|
+
## Read This Next
|
|
64
9
|
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
|
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
|
-
##
|
|
18
|
+
## Repo Map
|
|
71
19
|
|
|
72
|
-
|
|
|
73
|
-
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
import {JBOwnableOverrides} from "@bananapus/ownable-v6/src/JBOwnableOverrides.sol";
|
|
30
|
+
## Reference Files
|
|
158
31
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
IJBPermissions permissions,
|
|
167
|
-
IJBProjects projects,
|
|
168
|
-
address initialOwner,
|
|
169
|
-
uint88 initialProjectIdOwner
|
|
170
|
-
) JBOwnableOverrides(permissions, projects, initialOwner, initialProjectIdOwner) {}
|
|
35
|
+
## Working Rules
|
|
171
36
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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.
|