@bananapus/ownable-v6 0.0.31 → 0.0.32

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
@@ -50,6 +50,8 @@ This package is a small ownership adapter:
50
50
  - ownership may resolve to a project NFT holder instead of a fixed address, so caching `owner()` off-chain can go stale
51
51
  - `owner()` can resolve to `address(0)` if the referenced project NFT is invalid or unreadable, which effectively renounces the contract
52
52
  - delegated operator access depends on a chosen permission ID, not on a generic admin role
53
+ - explicit ownership transfers reset the permission ID, but project NFT transfers do not mutate stored owner data
54
+ - a project NFT round trip back to the owner who last set `permissionId` can reactivate that owner's still-granted delegates
53
55
  - ownership transfer and permission-ID updates are part of the security model, not just convenience helpers
54
56
 
55
57
  ## Where State Lives
@@ -64,6 +66,8 @@ This package is a small ownership adapter:
64
66
  2. `test/OwnableAttacks.t.sol`
65
67
  3. `test/RegressionUnmintedProjectHijack.t.sol`
66
68
  4. `test/regression/BurnLockProtection.t.sol`
69
+ 5. `test/regression/PermissionIdNFTTransfer.t.sol`
70
+ 6. `test/audit/CodexNemesisPermissionReactivation.t.sol`
67
71
 
68
72
  ## Install
69
73
 
@@ -95,7 +99,8 @@ test/
95
99
 
96
100
  - if ownership is tied to a project NFT and that NFT becomes unreachable, the contract is effectively locked
97
101
  - delegated access depends on a chosen permission ID, so bad permission selection is an operational risk
98
- - permission IDs reset on ownership transfer, which is safer by default but easy to miss
102
+ - permission IDs reset on explicit ownership transfers; project NFT transfers leave the ID stored but stale unless the
103
+ resolved owner still matches the owner who set it
99
104
  - transferring ownership to a project validates that the project exists at transfer time, but later project invalidation can still collapse effective ownership to `address(0)`
100
105
 
101
106
  ## For AI Agents
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@bananapus/ownable-v6",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/Bananapus/nana-ownable-v6"
7
+ "url": "git+https://github.com/Bananapus/nana-ownable-v6.git"
8
8
  },
9
9
  "files": [
10
10
  "foundry.toml",
@@ -17,7 +17,7 @@
17
17
  "node": ">=20.0.0"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/core-v6": "^0.0.66",
20
+ "@bananapus/core-v6": "^0.0.72",
21
21
  "@bananapus/permission-ids-v6": "^0.0.27",
22
22
  "@openzeppelin/contracts": "5.6.1"
23
23
  },
@@ -3,8 +3,10 @@
3
3
  ## Change Checklist
4
4
 
5
5
  - If you edit owner resolution, verify both direct ownership and project-owned cases.
6
- - If you edit permission handling, verify transfer-time reset behavior.
7
- - If an integration expects long-lived delegated access, confirm whether the transfer-reset rule invalidates that assumption.
6
+ - If you edit permission handling, verify explicit transfer resets, project NFT transfer staleness, and NFT round-trip
7
+ reactivation.
8
+ - If an integration expects long-lived delegated access, confirm whether explicit transfers clear it or project NFT
9
+ transfers merely make it stale.
8
10
  - If the change touches project ownership, check unminted-project and burn-lock regressions before assuming the happy-path tests are enough.
9
11
 
10
12
  ## Common Failure Modes
@@ -12,3 +14,4 @@
12
14
  - Integrations assume delegated operators survive ownership transfer.
13
15
  - Bugs are blamed on this repo when the underlying project NFT ownership changed upstream.
14
16
  - A project-owned contract is treated like an address-owned contract and the wrong actor is allowed through `onlyOwner`.
17
+ - Operators are assumed inactive after a project NFT round trip even though the prior owner's grants can reactivate.
@@ -9,11 +9,12 @@
9
9
 
10
10
  - Effective-owner resolution: ownership may follow a project NFT rather than a fixed address.
11
11
  - Delegated `onlyOwner` permissions: the chosen permission ID changes who can administer a contract.
12
- - Transfer semantics: permission IDs reset on transfer, which is safer but easy to forget.
12
+ - Transfer semantics: explicit ownable transfers reset permission IDs, while project NFT transfers preserve the stored
13
+ ID and rely on `_permissionOwner` to decide whether it is effective.
13
14
 
14
15
  ## Tests To Trust First
15
16
 
16
17
  - [`test/Ownable.t.sol`](../test/Ownable.t.sol) for baseline behavior.
17
18
  - [`test/OwnableEdgeCases.t.sol`](../test/OwnableEdgeCases.t.sol) and [`test/OwnableAttacks.t.sol`](../test/OwnableAttacks.t.sol) for edge and adversarial cases.
18
19
  - [`test/OwnableInvariantTests.sol`](../test/OwnableInvariantTests.sol) for broader invariants.
19
- - [`test/regression/BurnLockProtection.t.sol`](../test/regression/BurnLockProtection.t.sol), [`test/regression/ZeroAddressValidation.t.sol`](../test/regression/ZeroAddressValidation.t.sol), and [`test/RegressionUnmintedProjectHijack.t.sol`](../test/RegressionUnmintedProjectHijack.t.sol) for the regressions most likely to matter in review.
20
+ - [`test/regression/BurnLockProtection.t.sol`](../test/regression/BurnLockProtection.t.sol), [`test/RegressionUnmintedProjectHijack.t.sol`](../test/RegressionUnmintedProjectHijack.t.sol), [`test/regression/PermissionIdNFTTransfer.t.sol`](../test/regression/PermissionIdNFTTransfer.t.sol), and [`test/audit/CodexNemesisPermissionReactivation.t.sol`](../test/audit/CodexNemesisPermissionReactivation.t.sol) for the regressions most likely to matter in review.
package/src/JBOwnable.sol CHANGED
@@ -8,7 +8,7 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
8
8
  import {JBOwnableOverrides} from "./JBOwnableOverrides.sol";
9
9
 
10
10
  /// @notice Juicebox-aware ownership for any contract. Inherit this and apply the `onlyOwner` modifier to restrict
11
- /// functions to the project owner, a fixed address, or anyone the owner has granted permission to via `JBPermissions`.
11
+ /// functions to the project owner, a fixed address, or an effective delegate authorized through `JBPermissions`.
12
12
  /// @dev Ownership resolves dynamically: if `JBOwner.projectId` is set, the holder of that project's ERC-721 NFT is
13
13
  /// the owner. If `projectId` is 0, the stored `JBOwner.owner` address is used instead. The owner can delegate access
14
14
  /// to other addresses by setting a `permissionId` and granting that permission through `JBPermissions`.
@@ -50,11 +50,11 @@ contract JBOwnable is JBOwnableOverrides {
50
50
  // ------------------------ internal functions ----------------------- //
51
51
  //*********************************************************************//
52
52
 
53
- /// @notice Either `newOwner` or `newProjectId` is non-zero or both are zero. But they can never both be non-zero.
54
- /// @dev This function exists because some contracts need to deploy contracts for a project before the project's NFT
55
- /// has been minted, so the transfer event resolves the project's current owner at emission time.
56
- /// @dev Unlike `_transferOwnership` (which uses try-catch to resolve the *old* owner in case its project NFT was
57
- /// burned), this function resolves the *new* owner's current address for event purposes only.
53
+ /// @notice Emits the ownership transfer event after resolving the visible new owner address.
54
+ /// @dev Constructor pre-binding can point ownership at a project before the project's NFT has been minted, so the
55
+ /// event resolves the project's current owner at emission time.
56
+ /// @dev Unlike `_transferOwnership` (which uses try-catch to resolve the *old* owner in case its project NFT is
57
+ /// unreadable), this function resolves the *new* owner's current address for event purposes only.
58
58
  /// If the new project NFT does not exist yet, the event uses `address(0)` until ownership can resolve normally.
59
59
  function _emitTransferEvent(
60
60
  address previousOwner,
@@ -70,8 +70,7 @@ contract JBOwnable is JBOwnableOverrides {
70
70
  try PROJECTS.ownerOf(newProjectId) returns (address projectOwner) {
71
71
  resolvedNewOwner = projectOwner;
72
72
  } catch {
73
- // Allow constructor-time handoff to an unminted project. Ownership resolves dynamically
74
- // once the project NFT exists, so the transfer event uses address(0) until then.
73
+ // Pre-bound future projects have no visible owner yet, so the event reports address(0).
75
74
  resolvedNewOwner = address(0);
76
75
  }
77
76
  }
@@ -13,9 +13,9 @@ import {JBOwner} from "./structs/JBOwner.sol";
13
13
  /// @notice Abstract base implementing Juicebox-aware ownership resolution, transfer, and permission delegation.
14
14
  /// Ownership is either address-based (a fixed EOA/contract) or project-based (whoever holds the project's ERC-721
15
15
  /// NFT). The owner can delegate access to other addresses by configuring a `permissionId` in `JBPermissions`.
16
- /// @dev Stale permission detection: when ownership changes (e.g. project NFT transferred), the `permissionId` is
17
- /// effectively ignored until the new owner explicitly re-sets it preventing the previous owner's delegates from
18
- /// retaining access.
16
+ /// @dev Project NFT transfers do not update stored owner data. A nonzero `permissionId` is only effective while the
17
+ /// resolved owner still equals `_permissionOwner`, the owner who last set that ID. If the NFT leaves and later returns
18
+ /// to that owner, their still-granted delegate permissions become effective again.
19
19
  abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
20
20
  //*********************************************************************//
21
21
  // --------------------------- custom errors ------------------------- //
@@ -49,8 +49,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
49
49
  // -------------------- internal stored properties ------------------- //
50
50
  //*********************************************************************//
51
51
 
52
- /// @notice The resolved owner address at the time permissionId was last set.
53
- /// @dev Used to detect stale permissions after ownership changes (e.g., NFT transfer).
52
+ /// @notice The resolved owner address at the time `permissionId` was last set.
53
+ /// @dev Used to ignore delegated permissions while project ownership is held by someone else.
54
54
  address internal _permissionOwner;
55
55
 
56
56
  //*********************************************************************//
@@ -62,10 +62,9 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
62
62
  /// To restrict access to a specific address, pass that address as the `initialOwner` and `0` as the
63
63
  /// `initialProjectIdOwner`.
64
64
  /// @dev The owner can give owner access to other addresses through the `permissions` contract.
65
- /// @dev If `initialProjectIdOwner` references a project ID that has not yet been minted, all ownership checks will
66
- /// revert until that project is created, leaving the contract unusable. Deployers must ensure that the referenced
67
- /// project is minted before or atomically with this contract's deployment — this is a deployment trust
68
- /// assumption.
65
+ /// @dev If `initialProjectIdOwner` references an unminted project, `owner()` resolves to `address(0)` and
66
+ /// owner-gated calls revert until that project is created. The first account to mint that project becomes the
67
+ /// effective owner, so deployers must control the mint sequence.
69
68
  /// @param permissions A contract storing permissions. Assumed to be a valid deployment-time dependency.
70
69
  /// @param projects Mints ERC-721s that represent project ownership and transfers. Assumed to be a valid
71
70
  /// deployment-time dependency.
@@ -82,18 +81,14 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
82
81
  {
83
82
  PROJECTS = projects;
84
83
 
85
- // We force the inheriting contract to set an owner, as there is a low chance someone will use `JBOwnable` to
86
- // create an unowned contract.
87
- // It's more likely both were accidentally set to `0`. If you really want an unowned contract, set the owner to
88
- // an address and call `renounceOwnership()` in the constructor body.
84
+ // Require an initial owner. To deploy unowned on purpose, deploy with an address owner and call
85
+ // `renounceOwnership()` from the inheriting constructor.
89
86
  if (initialProjectIdOwner == 0 && initialOwner == address(0)) {
90
87
  revert JBOwnableOverrides_InvalidNewOwner({newOwner: initialOwner, projectId: initialProjectIdOwner});
91
88
  }
92
89
 
93
- // No explicit project existence check here if `initialProjectIdOwner` refers to an unminted project,
94
- // `owner()` will resolve via `PROJECTS.ownerOf()`, which reverts for non-existent tokens. The try-catch
95
- // in `owner()` treats this as renounced (returns address(0)), effectively locking the contract until
96
- // the project is minted. This is acceptable because deployers control the constructor arguments.
90
+ // Constructors may pre-bind ownership to a project that will be minted later. Until then, `owner()` returns
91
+ // address(0); once minted, ownership follows whoever received that project NFT.
97
92
  _transferOwnership({newOwner: initialOwner, projectId: initialProjectIdOwner});
98
93
  }
99
94
 
@@ -103,9 +98,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
103
98
 
104
99
  /// @notice Returns the current owner's address. If ownership is project-based, this dynamically resolves to
105
100
  /// whoever holds the project's ERC-721 NFT right now.
106
- /// @dev If `projectId` is non-zero, resolves via `PROJECTS.ownerOf()`. If that call reverts (e.g., burned NFT),
107
- /// returns `address(0)` effectively treating the contract as renounced. `JBProjects` V6 has no burn function,
108
- /// so this is a defensive measure only.
101
+ /// @dev If `projectId` is non-zero, resolves via `PROJECTS.ownerOf()`. If that call reverts, returns
102
+ /// `address(0)`, making owner-gated functions fail closed.
109
103
  function owner() public view virtual returns (address) {
110
104
  JBOwner memory ownerInfo = jbOwner;
111
105
 
@@ -113,8 +107,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
113
107
  return ownerInfo.owner;
114
108
  }
115
109
 
116
- // Use try-catch to gracefully handle the case where the project NFT no longer exists.
117
- // If ownerOf reverts, the contract is effectively renounced (returns address(0)).
110
+ // If the project owner cannot be read, expose the owner as zero instead of bubbling the upstream revert.
118
111
  try PROJECTS.ownerOf(ownerInfo.projectId) returns (address projectOwner) {
119
112
  return projectOwner;
120
113
  } catch {
@@ -127,10 +120,11 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
127
120
  //*********************************************************************//
128
121
 
129
122
  /// @notice Reverts if the caller is not the owner (or an authorized delegate when `permissionId` is set).
130
- /// @dev If `projectId` is non-zero and `PROJECTS.ownerOf()` reverts (e.g., burned NFT), the resolved owner is
131
- /// `address(0)`, causing all `_checkOwner` calls to revert — equivalent to a renounced contract.
132
- /// @dev Stale permission detection: if the resolved owner differs from `_permissionOwner` (set when
133
- /// `setPermissionId` was last called), delegation is disabled until the new owner re-configures it.
123
+ /// @dev If `projectId` is non-zero and `PROJECTS.ownerOf()` reverts, the resolved owner is `address(0)`, causing
124
+ /// all `_checkOwner` calls to revert.
125
+ /// @dev A nonzero `permissionId` only delegates while the current resolved owner equals `_permissionOwner`.
126
+ /// Project NFT transfers therefore disable delegation for the new holder until they call `setPermissionId()`;
127
+ /// returning the NFT to the original `_permissionOwner` can reactivate their old delegate grants.
134
128
  function _checkOwner() internal view virtual {
135
129
  JBOwner memory ownerInfo = jbOwner;
136
130
 
@@ -138,7 +132,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
138
132
  if (ownerInfo.projectId == 0) {
139
133
  resolvedOwner = ownerInfo.owner;
140
134
  } else {
141
- // Use try-catch to gracefully handle the case where the project NFT no longer exists.
135
+ // Resolve the project owner dynamically; unreadable projects fail closed to address(0).
142
136
  try PROJECTS.ownerOf(ownerInfo.projectId) returns (address projectOwner) {
143
137
  resolvedOwner = projectOwner;
144
138
  } catch {
@@ -146,14 +140,13 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
146
140
  }
147
141
  }
148
142
 
149
- // Detect stale permissions: if ownership changed since permissionId was set
150
- // (e.g., project NFT transferred), treat permissionId as 0 (direct-owner-only).
143
+ // Ignore the stored permission ID while the project NFT is held by a different owner than the one who set it.
151
144
  uint8 effectivePermissionId = ownerInfo.permissionId;
152
145
  if (effectivePermissionId != 0 && resolvedOwner != _permissionOwner) {
153
146
  effectivePermissionId = 0;
154
147
  }
155
148
 
156
- // When permissionId is 0 (direct-owner-only mode), bypass the permission system entirely.
149
+ // When delegation is disabled or stale, bypass the permission system entirely.
157
150
  // This ensures ROOT operators cannot act as owner when delegation is disabled.
158
151
  if (effectivePermissionId == 0) {
159
152
  if (_msgSender() != resolvedOwner) {
@@ -182,8 +175,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
182
175
 
183
176
  /// @notice Configures which `JBPermissions` permission ID grants delegate access to `onlyOwner` functions.
184
177
  /// Set to 0 to disable delegation entirely (only the direct owner can call).
185
- /// @dev Can only be called by the current owner. Records the current owner so stale permissions are detected
186
- /// if ownership later changes.
178
+ /// @dev Can only be called by the current owner. Records the current owner so delegation is ignored while a
179
+ /// different owner holds the project NFT.
187
180
  /// @param permissionId The permission ID to use for `onlyOwner` delegation.
188
181
  function setPermissionId(uint8 permissionId) public virtual override {
189
182
  _checkOwner();
@@ -216,7 +209,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
216
209
  revert JBOwnableOverrides_InvalidNewOwner({newOwner: address(0), projectId: projectId});
217
210
  }
218
211
 
219
- // Make sure the project exists to prevent permanent loss of contract control.
212
+ // Public project transfers require an already-minted project. Constructor pre-binding is the only path that
213
+ // can point at a future project ID.
220
214
  if (projectId > PROJECTS.count()) {
221
215
  revert JBOwnableOverrides_ProjectDoesNotExist({projectId: projectId, projectCount: PROJECTS.count()});
222
216
  }
@@ -229,7 +223,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
229
223
  // ------------------------ internal functions ----------------------- //
230
224
  //*********************************************************************//
231
225
 
232
- /// @notice Either `newOwner` or `newProjectId` is non-zero or both are zero. But they can never both be non-zero.
226
+ /// @notice Emits the ownership transfer event after resolving the visible new owner address.
233
227
  /// @dev This function exists because some contracts need to deploy contracts for a project before the project's NFT
234
228
  /// has been minted, so the transfer event resolves the project's current owner at emission time.
235
229
  /// @param previousOwner The address of the previous owner.
@@ -237,7 +231,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
237
231
  /// @param newProjectId The ID of the new owning project (zero if transferring to an address).
238
232
  function _emitTransferEvent(address previousOwner, address newOwner, uint88 newProjectId) internal virtual;
239
233
 
240
- /// @notice Sets the permission ID the owner can use to give other addresses owner access.
234
+ /// @notice Sets the permission ID the current owner can use to delegate owner access.
241
235
  /// @dev Internal function without access restriction.
242
236
  /// @param permissionId The permission ID to use for `onlyOwner`.
243
237
  function _setPermissionId(uint8 permissionId) internal virtual {
@@ -252,20 +246,21 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
252
246
  _transferOwnership({newOwner: newOwner, projectId: 0});
253
247
  }
254
248
 
255
- /// @notice Transfers this contract's ownership to an address (`newOwner`) OR a Juicebox project (`projectId`).
249
+ /// @notice Transfers this contract's ownership to either an address (`newOwner`) or a Juicebox project
250
+ /// (`projectId`).
256
251
  /// @dev Updates this contract's `JBOwner` owner information and resets the `JBOwner.permissionId`.
257
252
  /// @dev If both `newOwner` and `projectId` are set, this will revert.
258
253
  /// @dev Internal function without access restriction.
259
254
  /// @param newOwner The address that should become this contract's owner.
260
255
  /// @param projectId The ID of the project whose owner should become this contract's owner.
261
256
  function _transferOwnership(address newOwner, uint88 projectId) internal virtual {
262
- // Can't set both a new owner and a new project ID.
257
+ // Ownership has exactly one live mode: address owner, project owner, or neither after renounce.
263
258
  if (projectId != 0 && newOwner != address(0)) {
264
259
  revert JBOwnableOverrides_InvalidNewOwner({newOwner: newOwner, projectId: projectId});
265
260
  }
266
- // Load the owner information from storage.
261
+ // Snapshot the current owner configuration before replacing it.
267
262
  JBOwner memory ownerInfo = jbOwner;
268
- // Get the address of the old owner. Use try-catch for project-based ownership in case the NFT was burned.
263
+ // Resolve the previous owner for the event; unreadable project ownership is reported as address(0).
269
264
  address oldOwner;
270
265
  if (ownerInfo.projectId == 0) {
271
266
  oldOwner = ownerInfo.owner;
@@ -276,8 +271,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
276
271
  oldOwner = address(0);
277
272
  }
278
273
  }
279
- // Update the stored owner information to the new owner and reset the `permissionId`.
280
- // This is to prevent permissions clashes for the new user/owner.
274
+ // Explicit ownership transfers clear delegated access and the owner who authorized it.
281
275
  jbOwner = JBOwner({owner: newOwner, projectId: projectId, permissionId: 0});
282
276
  _permissionOwner = address(0);
283
277
  // Emit a transfer event with the new owner's address.
@@ -22,10 +22,11 @@ interface IJBOwnable {
22
22
  /// @return projects The `IJBProjects` contract.
23
23
  function PROJECTS() external view returns (IJBProjects projects);
24
24
 
25
- /// @notice The current ownership state — who owns this contract and how permission delegation is configured.
25
+ /// @notice The stored ownership state and delegation policy.
26
26
  /// @return owner The owner address (used when `projectId` is 0).
27
27
  /// @return projectId The Juicebox project whose NFT holder is the owner (0 if address-based ownership).
28
- /// @return permissionId The permission ID that delegates can use to act as owner via `JBPermissions`.
28
+ /// @return permissionId The stored permission ID. It only delegates while the resolved owner matches the owner who
29
+ /// last set it.
29
30
  function jbOwner() external view returns (address owner, uint88 projectId, uint8 permissionId);
30
31
 
31
32
  /// @notice Returns the current owner's address.
@@ -5,8 +5,8 @@ pragma solidity ^0.8.0;
5
5
  /// @custom:member owner The owner address — only used when `projectId` is 0 (address-based ownership mode).
6
6
  /// @custom:member projectId If non-zero, the holder of this Juicebox project's ERC-721 NFT is the owner. When set,
7
7
  /// the `owner` field is ignored and ownership resolves dynamically via `JBProjects.ownerOf(projectId)`.
8
- /// @custom:member permissionId The permission ID that delegates can hold (via `JBPermissions`) to act as owner. Set
9
- /// to 0 to disable delegation entirely only the direct owner can call `onlyOwner` functions.
8
+ /// @custom:member permissionId The permission ID that delegates can hold via `JBPermissions`. Nonzero IDs only count
9
+ /// while the resolved owner matches the owner who last set the ID. Set to 0 to disable delegation entirely.
10
10
  struct JBOwner {
11
11
  address owner;
12
12
  uint88 projectId;