@bananapus/ownable-v6 0.0.16 → 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/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 +25 -1
- 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/JBOwnable.sol +14 -7
- package/src/JBOwnableOverrides.sol +4 -0
- package/src/structs/JBOwner.sol +0 -1
- package/test/CodexUnmintedProjectHijack.t.sol +45 -0
- package/test/OwnableEdgeCases.t.sol +21 -5
- 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,22 +1,45 @@
|
|
|
1
|
-
#
|
|
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
|
-
##
|
|
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.
|
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
|
|
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 —
|
|
30
|
-
pragma solidity
|
|
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.
|
|
138
|
-
5.
|
|
139
|
-
6.
|
|
140
|
-
7.
|
|
141
|
-
8.
|
|
142
|
-
9.
|
|
143
|
-
10.
|
|
144
|
-
11.
|
|
145
|
-
12.
|
|
146
|
-
13.
|
|
147
|
-
14.
|
|
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.
|
|
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.
|