@bananapus/ownable-v6
v0.0.9
Published
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.
Readme
Juicebox Ownable
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.
This is a variation on OpenZeppelin Ownable that adds:
- 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.
- The ability to grant other addresses
onlyOwneraccess usingJBPermissions, with a configurablepermissionId. JBPermissionedbase class with support for OpenZeppelinContext(enabling optional meta-transaction support).
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.
Forked from jbx-protocol/juice-ownable.
If you have questions, take a look at the core protocol contracts and the documentation first, or reach out on Discord.
Architecture
JBOwnable
└── JBOwnableOverrides (abstract)
├── Context (OpenZeppelin)
├── JBPermissioned (nana-core-v6)
└── IJBOwnable (interface)| Contract | Description |
|----------|-------------|
| JBOwnable | Concrete implementation. Provides the onlyOwner modifier and emits OwnershipTransferred events (resolving project NFT holders at emission time). Inherit this in your contract. |
| JBOwnableOverrides | 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). |
Supporting Types
| Type | Location | Description |
|------|----------|-------------|
| JBOwner | src/structs/ | Struct packing address owner (160 bits), uint88 projectId (88 bits), and uint8 permissionId (8 bits) into a single 256-bit storage slot. |
| IJBOwnable | src/interfaces/ | Interface exposing ownership queries, transfers, renunciation, permission ID management, and events. |
Ownership Modes
- Project ownership -- If
JBOwner.projectIdis nonzero, the current holder of thatJBProjectsERC-721 is the owner. Ownership automatically follows the NFT: when the NFT is transferred,owner()immediately reflects the new holder without any additional transaction. - Address ownership -- If
projectIdis zero,JBOwner.owneris the owner directly. - Delegated access -- The owner can grant other addresses access via
JBPermissions.setPermissionsFor(...)using the configuredpermissionId. The owner must first callsetPermissionId(uint8)to set which permission ID represents owner-level access. - Renounced -- After calling
renounceOwnership(), bothownerandprojectIdare set to zero. No one can callonlyOwnerfunctions. This is irreversible.
The permissionId resets to 0 on every ownership transfer to prevent permission clashes for the new owner.
Events
| Event | Emitted By |
|-------|-----------|
| OwnershipTransferred(address indexed previousOwner, address indexed newOwner, address caller) | _emitTransferEvent (called on every ownership change) |
| PermissionIdChanged(uint8 newId, address caller) | _setPermissionId |
Errors
| Error | Thrown When |
|-------|-----------|
| 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. |
| JBOwnableOverrides_ProjectDoesNotExist() | transferOwnershipToProject receives a projectId greater than PROJECTS.count(). |
| JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner() | Constructor receives a non-zero initialProjectIdOwner with projects set to address(0). |
How Ownership Resolution Works
When _checkOwner() is called (by the onlyOwner modifier), it:
- Reads the
JBOwnerstruct from storage. - Resolves the owner address: if
projectId != 0, callsPROJECTS.ownerOf(projectId)via try-catch (returnsaddress(0)if the call reverts, e.g., burned NFT); otherwise uses the storedowneraddress. - Calls
_requirePermissionFrom(account, projectId, permissionId)fromJBPermissioned, which passes if the caller is:- The resolved owner address, OR
- An address granted the configured
permissionIdon the relevant project viaJBPermissions, OR - An address granted the ROOT permission (permission ID 1) on the relevant project via
JBPermissions.
How Delegated Access Works
- The owner calls
setPermissionId(uint8)on theJBOwnablecontract to configure which permission ID represents owner-level access. - The owner calls
JBPermissions.setPermissionsFor(...)to grant that permission ID to specific addresses on the relevant project. - Those addresses can now call
onlyOwnerfunctions. - 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.
Install
npm installDevelop
| Command | Description |
|---------|-------------|
| forge build | Compile contracts |
| forge test | Run tests |
| forge coverage --match-path "./src/*.sol" --report lcov --report summary | Generate coverage report |
