@bananapus/721-hook-v6 0.0.70 → 0.0.73

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
@@ -10,7 +10,7 @@
10
10
  - [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md) — what to focus on for a security audit and how to start.
11
11
  - [SKILLS.md](./SKILLS.md) — implementation nuances, gotchas, and reading order for working on this codebase.
12
12
  - [STYLE_GUIDE.md](./STYLE_GUIDE.md) — Solidity conventions and repo layout used across the V6 ecosystem.
13
- - [CHANGELOG.md](./CHANGELOG.md) v5 v6 ABI and behavior deltas.
13
+ - [CHANGELOG.md](./CHANGELOG.md) - V5 to V6 ABI and behavior deltas.
14
14
  - [references/runtime.md](./references/runtime.md) — contract roles, the runtime pay and cash-out path, and high-risk areas.
15
15
  - [references/operations.md](./references/operations.md) — deployment surface, change checklist, and common failure modes.
16
16
 
@@ -38,7 +38,7 @@ This repo does more than "mint NFTs on pay." It changes how payment value, tier
38
38
  | `JB721TiersHookDeployer` | Clone factory for deploying a hook for an existing project. |
39
39
  | `JB721TiersHookProjectDeployer` | Convenience deployer for launching a project with a hook already wired in. |
40
40
  | `JB721Hook` | Abstract base for 721 pay and cash-out hook behavior. |
41
- | `JB721Checkpoints` | Per-hook IVotes checkpoint module. Tracks historical owner checkpoints, per-tier eligible voting units (`getPastTierVotingUnits`) for tier-scoped reward distribution, and active delegated vote totals (`getPastTotalActiveVotes`). |
41
+ | `JB721Checkpoints` | Per-hook IVotes checkpoint module. Tracks historical owner checkpoints, per-tier owner-tracked voting units (`getPastTierVotingUnits`), global active vote totals (`getPastTotalActiveVotes`), per-tier active vote totals (`getPastTotalTierActiveVotes`), and per-account active tier voting units (`getPastAccountTierActiveVotes`). |
42
42
 
43
43
  ## Mental model
44
44
 
@@ -64,8 +64,9 @@ If a bug affects supply, reserve minting, or tier lookup, it usually lives in th
64
64
  - custom token URI resolvers should be treated as part of the trusted surface
65
65
  - adding a 721 hook through a deployer is easy; carrying the right ruleset behavior forward is where mistakes happen
66
66
  - projects should be explicit about whether the hook affects pay, cash out, or only metadata-facing paths
67
- - per-tier eligible voting units are queryable via `getPastTierVotingUnits(tierId, blockNumber)` for tier-scoped reward denominators, but minting alone does not enroll a token: a token only counts toward that total once it is enrolled (`delegate(address, uint256[])`) or transferred for the first time, and stops counting when burned
68
- - active delegated vote totals are queryable via `getPastTotalActiveVotes(blockNumber)` and `getTotalActiveVotes()`. These totals include only voting units held by accounts with a nonzero delegate, so a token in undelegated custody does not count; this is a governance/reward-participation primitive, not the owner-based tier reward denominator.
67
+ - per-tier owner-tracked voting units are queryable via `getPastTierVotingUnits(tierId, blockNumber)`: mints, transfers, and burns write ownership history, so the trace follows owned units regardless of delegation
68
+ - active delegated vote totals are queryable globally via `getPastTotalActiveVotes(blockNumber)` / `getTotalActiveVotes()` and per tier via `getPastTotalTierActiveVotes(tierId, blockNumber)` / `getTotalTierActiveVotes(tierId)`. These totals include only voting units held by accounts with a nonzero delegate, so a token in undelegated custody does not count and returned tokens become active again if the holder's delegation is still set.
69
+ - per-account active tier voting units are queryable via `getPastAccountTierActiveVotes(account, tierId, blockNumber)`. This follows the account holding the tier units, not the delegate receiving voting power, so reward distributors can cap tier-scoped claims against the holder's active units even when votes are delegated to another address.
69
70
 
70
71
  ## Where state lives
71
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.70",
3
+ "version": "0.0.73",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,12 +25,12 @@
25
25
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
26
26
  },
27
27
  "dependencies": {
28
- "@bananapus/address-registry-v6": "^0.0.33",
29
- "@bananapus/core-v6": "^0.0.85",
30
- "@bananapus/ownable-v6": "^0.0.36",
31
- "@bananapus/permission-ids-v6": "^0.0.29",
28
+ "@bananapus/address-registry-v6": "^0.0.34",
29
+ "@bananapus/core-v6": "^0.0.86",
30
+ "@bananapus/ownable-v6": "^0.0.37",
31
+ "@bananapus/permission-ids-v6": "^0.0.30",
32
32
  "@openzeppelin/contracts": "5.6.1",
33
- "@prb/math": "4.1.1",
33
+ "@prb/math": "4.1.2",
34
34
  "solady": "0.1.26"
35
35
  },
36
36
  "devDependencies": {
@@ -12,8 +12,8 @@
12
12
  - If you edit tier config or metadata behavior, inspect the corresponding structs and interfaces in `src/structs/` and `src/interfaces/`.
13
13
  - If you edit reserve behavior, verify pending reserve counts, default reserve beneficiary semantics, and cash-out denominator effects together.
14
14
  - If you edit discount behavior, verify mint price and cash-out weight separately. They are intentionally not the same quantity.
15
- - If you touch checkpoint, `onTransfer`, or `delegate` behavior, verify the per-tier eligible-voting-units trace (`_tierEligibleUnitsOf`, read via `getPastTierVotingUnits`) still moves only on eligibility changes: increment on enrollment or a token's first transfer, decrement on burn, and nothing on mint (so the mint path keeps its zero added checkpoint gas). Keep it in lockstep with `ownerOfAt` eligibility.
16
- - Also verify the active delegated vote trace (`_activeSupplyCheckpoints`, read via `getPastTotalActiveVotes` and `getTotalActiveVotes`) moves only when voting units enter or leave nonzero delegation. It is separate from owner-based tier reward eligibility.
15
+ - If you touch checkpoint, `onTransfer`, or `delegate` behavior, verify the per-tier eligible-voting-units trace (`_tierEligibleUnitsOf`, read via `getPastTierVotingUnits`) still moves only on eligibility changes: increment on mint or first checkpoint backfill, decrement on burn, and stay unchanged on ordinary transfers. Keep it in lockstep with `ownerOfAt` eligibility.
16
+ - Also verify the active delegated vote traces (`_activeSupplyCheckpoints`, `_tierActiveSupplyCheckpointsOf`, and `_accountTierActiveVotesOf`) move only when voting units enter or leave nonzero delegation. They are separate from owner-based tier reward eligibility.
17
17
  - If you touch permissions, verify the caller path and permission constants still line up with the downstream ecosystem package that defines them.
18
18
  - If you touch URI behavior, confirm whether the issue belongs in this repo or in a downstream resolver contract that the hook calls.
19
19
 
@@ -29,7 +29,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
29
29
  /// @notice Thrown when `initialize` is called on a module whose hook has already been set.
30
30
  error JB721Checkpoints_AlreadyInitialized(address hook);
31
31
 
32
- /// @notice Thrown when the caller tries to enroll a token they do not currently own.
32
+ /// @notice Thrown when the caller tries to backfill a token they do not currently own.
33
33
  error JB721Checkpoints_NotOwner(uint256 tokenId, address caller);
34
34
 
35
35
  /// @notice Thrown when a hook-only function is called by an address other than the module's hook.
@@ -40,6 +40,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
40
40
  //*********************************************************************//
41
41
 
42
42
  /// @notice The store that holds tier and voting data for the hook's NFTs.
43
+ /// @dev All tier lookups are scoped by `hook`; this immutable only identifies the shared store contract.
43
44
  IJB721TiersHookStore public immutable override STORE;
44
45
 
45
46
  //*********************************************************************//
@@ -47,33 +48,50 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
47
48
  //*********************************************************************//
48
49
 
49
50
  /// @notice The hook that this module tracks voting power for.
51
+ /// @dev Clones set this once in `initialize`; the implementation uses `address(1)` as a locked sentinel.
50
52
  address public override hook;
51
53
 
52
54
  //*********************************************************************//
53
55
  // -------------------- internal stored properties ------------------- //
54
56
  //*********************************************************************//
55
57
 
56
- /// @notice Checkpointed token owners for historical reward eligibility. Written on enrollment or transfer.
58
+ /// @notice Checkpointed active voting units per account and tier.
59
+ /// @dev Maintained only for units held by accounts with nonzero delegates. Undelegated custody is not checkpointed
60
+ /// in this trace because those units are inactive for active-vote reward accounting.
61
+ /// @custom:param account The account to get historical active tier voting units for.
62
+ /// @custom:param tierId The tier to get historical active voting units for.
63
+ mapping(address account => mapping(uint256 tierId => Checkpoints.Trace160)) internal _accountTierActiveVotesOf;
64
+
65
+ /// @notice Checkpointed token owners for historical reward eligibility.
66
+ /// @dev Mint and transfer hooks write this automatically; `delegate` only backfills missing pre-upgrade history.
57
67
  /// @custom:param tokenId The token ID to get historical owner checkpoints for.
58
68
  mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
59
69
 
60
- /// @notice Checkpointed total eligible voting units per tier. A token contributes its tier voting units from the
61
- /// block it first gains an owner checkpoint (enrollment or first transfer) until it is burned. Mints write
62
- /// nothing, mirroring `_ownerCheckpointsOf` eligibility. Distributors read this as the tier-scoped denominator.
63
- /// @custom:param tierId The tier to get the historical eligible voting units for.
70
+ /// @notice Checkpointed total active voting units per tier.
71
+ /// @dev Maintained when delegation changes activate/deactivate all of an account's tier units, and when transfers
72
+ /// move one token's tier units between delegated and undelegated custody.
73
+ /// @custom:param tierId The tier to get the historical delegated voting units for.
74
+ mapping(uint256 tierId => Checkpoints.Trace160) internal _tierActiveSupplyCheckpointsOf;
75
+
76
+ /// @notice Checkpointed total owner-tracked voting units per tier.
77
+ /// @dev Increased on mint or backfill and decreased on burn. Transfers keep the total unchanged because the token
78
+ /// still has a nonzero owner.
79
+ /// @custom:param tierId The tier to get the historical owner-tracked voting units for.
64
80
  mapping(uint256 tierId => Checkpoints.Trace160) internal _tierEligibleUnitsOf;
65
81
 
66
82
  //*********************************************************************//
67
83
  // -------------------- private stored properties -------------------- //
68
84
  //*********************************************************************//
69
85
 
70
- /// @notice The total voting units currently delegated to nonzero delegates.
86
+ /// @notice Checkpointed total voting units held by accounts with nonzero delegates.
87
+ /// @dev Maintained by `_delegate` and `_transferVotingUnits` using the same clock as OZ `Votes`.
71
88
  Checkpoints.Trace208 private _activeSupplyCheckpoints;
72
89
 
73
90
  //*********************************************************************//
74
91
  // -------------------------- constructor ---------------------------- //
75
92
  //*********************************************************************//
76
93
 
94
+ /// @notice Initializes the checkpoint implementation and locks it against direct use.
77
95
  /// @dev The implementation contract is initialized in the constructor to prevent direct use. Clones are initialized
78
96
  /// via `initialize()`.
79
97
  /// @param store The store that holds tier data for each hook's NFTs.
@@ -86,26 +104,25 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
86
104
  // ---------------------- external transactions ---------------------- //
87
105
  //*********************************************************************//
88
106
 
89
- /// @notice Delegates voting power and enrolls tokens for distribution eligibility.
90
- /// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
91
- /// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
92
- /// distribution. The existing `delegate(address)` from OZ Votes still works for pure delegation without enrollment.
107
+ /// @notice Delegates voting power and backfills ownership history for listed tokens if needed.
108
+ /// @dev Mint and transfer hooks normally write owner checkpoints automatically. The token ID list keeps
109
+ /// pre-upgrade or otherwise uncheckpointed tokens recoverable while preserving the owner-only authorization check.
93
110
  /// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
94
- /// @param tokenIds The token IDs to enroll for distribution eligibility.
111
+ /// @param tokenIds The token IDs whose owner checkpoints should be backfilled if missing.
95
112
  function delegate(address delegatee, uint256[] calldata tokenIds) external override {
96
113
  // Delegate voting power (reuses OZ Votes internals).
97
114
  _delegate({account: msg.sender, delegatee: delegatee});
98
115
 
99
- // Write per-token owner checkpoints for distribution eligibility.
116
+ // Write any missing per-token owner checkpoints for historical reward eligibility.
100
117
  for (uint256 i; i < tokenIds.length;) {
101
118
  uint256 tokenId = tokenIds[i];
102
119
 
103
- // Only the current owner can enroll their tokens.
120
+ // Only the current owner can backfill their token's ownership checkpoint.
104
121
  if (IERC721(hook).ownerOf(tokenId) != msg.sender) {
105
122
  revert JB721Checkpoints_NotOwner({tokenId: tokenId, caller: msg.sender});
106
123
  }
107
124
 
108
- // Write an owner checkpoint if the token has none yet, and enroll its tier voting units.
125
+ // Write an owner checkpoint if the token has none yet, and track its tier voting units.
109
126
  if (_ownerCheckpointsOf[tokenId].length() == 0) {
110
127
  // forge-lint: disable-next-line(unsafe-typecast)
111
128
  _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(msg.sender)});
@@ -139,54 +156,129 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
139
156
  function onTransfer(address from, address to, uint256 tokenId) external override {
140
157
  if (msg.sender != hook) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: hook});
141
158
 
159
+ // Look up this token's tier ID and voting units once; both the owner and active traces need them.
160
+ uint256 tierId = STORE.tierIdOfToken(tokenId);
161
+
142
162
  // Look up this token's tier voting units (lightweight getter — avoids full tier struct construction).
143
163
  uint256 votingUnits = STORE.tierVotingUnitsOfTokenId({hook: hook, tokenId: tokenId});
144
164
 
145
- // On mint (`from == 0`) nothing is checkpointed: the token is ineligible until enrolled or transferred,
146
- // so neither the owner trace nor the per-tier eligible-units trace is written.
147
- if (from != address(0)) {
148
- Checkpoints.Trace160 storage ownerTrace = _ownerCheckpointsOf[tokenId];
149
- bool wasEligible = ownerTrace.length() != 0;
165
+ // Keep a reference to the token's historical owner trace.
166
+ Checkpoints.Trace160 storage ownerTrace = _ownerCheckpointsOf[tokenId];
167
+
168
+ // Existing deployments may have tokens that predate mint checkpointing; detect the first written owner.
169
+ bool wasEligible = ownerTrace.length() != 0;
150
170
 
171
+ // Mints, transfers, and burns all write ownership history so reward claims can prove snapshot ownership.
172
+ if (to != address(0) || from != address(0)) {
151
173
  // forge-lint: disable-next-line(unsafe-typecast)
152
174
  ownerTrace.push({key: uint96(block.number), value: uint160(to)});
175
+ }
153
176
 
154
- if (to == address(0)) {
155
- // Burn: remove the tier's units only if the token was already eligible.
156
- if (wasEligible) {
157
- _updateTierEligibleUnits({
158
- tierId: STORE.tierIdOfToken(tokenId), amount: votingUnits, increase: false
159
- });
160
- }
161
- } else if (!wasEligible) {
162
- // First transfer of a never-enrolled token makes it eligible: add the tier's units.
163
- _updateTierEligibleUnits({tierId: STORE.tierIdOfToken(tokenId), amount: votingUnits, increase: true});
177
+ if (from == address(0) && to != address(0)) {
178
+ // Mint: add the token's tier units to the owner-tracked tier supply.
179
+ _updateTierEligibleUnits({tierId: tierId, amount: votingUnits, increase: true});
180
+ } else if (to == address(0)) {
181
+ // Burn: remove the tier's units only if the token was already part of the owner-tracked supply.
182
+ if (wasEligible) {
183
+ _updateTierEligibleUnits({tierId: tierId, amount: votingUnits, increase: false});
164
184
  }
185
+ } else if (!wasEligible) {
186
+ // First transfer of a pre-upgrade uncheckpointed token makes its ownership history trackable.
187
+ _updateTierEligibleUnits({tierId: tierId, amount: votingUnits, increase: true});
165
188
  }
166
189
 
190
+ // The tier active total decreases if units leave an account that already has a nonzero delegate.
191
+ bool decreaseTierActiveVotes = from != address(0) && delegates(from) != address(0);
192
+
193
+ // The tier active total increases if units arrive at an account that already has a nonzero delegate.
194
+ bool increaseTierActiveVotes = to != address(0) && delegates(to) != address(0);
195
+
167
196
  // Move checkpointed voting power from the previous owner to the new owner.
168
197
  _transferVotingUnits({from: from, to: to, amount: votingUnits});
198
+
199
+ // If the sender was delegated, remove this token's tier units from the sender's active tier balance.
200
+ if (decreaseTierActiveVotes) {
201
+ _adjustAccountTierActiveVotes({account: from, tierId: tierId, amount: votingUnits, increase: false});
202
+ }
203
+
204
+ // If the receiver is delegated, add this token's tier units to the receiver's active tier balance.
205
+ if (increaseTierActiveVotes) {
206
+ _adjustAccountTierActiveVotes({account: to, tierId: tierId, amount: votingUnits, increase: true});
207
+ }
208
+
209
+ // If both sides are delegated or both sides are undelegated, this tier's active total is unchanged.
210
+ if (decreaseTierActiveVotes != increaseTierActiveVotes) {
211
+ // Otherwise apply the one-sided active-tier delta implied by the receiver's delegated status.
212
+ _adjustTotalTierActiveVotes({tierId: tierId, amount: votingUnits, increase: increaseTierActiveVotes});
213
+ }
169
214
  }
170
215
 
171
216
  //*********************************************************************//
172
217
  // ----------------------- external views ---------------------------- //
173
218
  //*********************************************************************//
174
219
 
175
- /// @notice The total eligible voting units of a tier at a past block.
176
- /// @param tierId The tier to get the eligible voting units of.
220
+ /// @notice The delegated voting units held by an account in a tier at a past block.
221
+ /// @dev Counts only tier voting units held by `account` while `account` had a nonzero delegate.
222
+ /// @param account The account to get the delegated tier voting units of.
223
+ /// @param tierId The tier to get the delegated voting units of.
224
+ /// @param blockNumber The past block number to look up.
225
+ /// @return activeVotes The account's delegated tier voting units at `blockNumber`.
226
+ function getPastAccountTierActiveVotes(
227
+ address account,
228
+ uint256 tierId,
229
+ uint256 blockNumber
230
+ )
231
+ external
232
+ view
233
+ override
234
+ returns (uint256 activeVotes)
235
+ {
236
+ // forge-lint: disable-next-line(unsafe-typecast)
237
+ activeVotes =
238
+ _accountTierActiveVotesOf[account][tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
239
+ }
240
+
241
+ /// @notice The total owner-checkpointed voting units of a tier at a past block.
242
+ /// @param tierId The tier to get the owner-checkpointed voting units of.
177
243
  /// @param blockNumber The block number to look up (must be strictly in the past).
178
- /// @return The tier's eligible voting units at `blockNumber`.
179
- function getPastTierVotingUnits(uint256 tierId, uint256 blockNumber) external view override returns (uint256) {
244
+ /// @return votingUnits The tier's owner-checkpointed voting units at `blockNumber`.
245
+ function getPastTierVotingUnits(
246
+ uint256 tierId,
247
+ uint256 blockNumber
248
+ )
249
+ external
250
+ view
251
+ override
252
+ returns (uint256 votingUnits)
253
+ {
180
254
  // forge-lint: disable-next-line(unsafe-typecast)
181
- return _tierEligibleUnitsOf[tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
255
+ votingUnits = _tierEligibleUnitsOf[tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
182
256
  }
183
257
 
184
258
  /// @notice The total delegated voting units at a past block.
185
259
  /// @dev This tracks delegated vote participation and is separate from tier reward eligibility.
186
- /// @param timepoint The past block number to look up.
187
- /// @return activeVotes The total voting units delegated to nonzero delegates at `timepoint`.
188
- function getPastTotalActiveVotes(uint256 timepoint) external view override returns (uint256 activeVotes) {
189
- activeVotes = _activeSupplyCheckpoints.upperLookupRecent(_validateTimepoint(timepoint));
260
+ /// @param blockNumber The past block number to look up.
261
+ /// @return activeVotes The total voting units delegated to nonzero delegates at `blockNumber`.
262
+ function getPastTotalActiveVotes(uint256 blockNumber) external view override returns (uint256 activeVotes) {
263
+ activeVotes = _activeSupplyCheckpoints.upperLookupRecent(_validateTimepoint(blockNumber));
264
+ }
265
+
266
+ /// @notice The total delegated voting units of a tier at a past block.
267
+ /// @dev Counts only tier voting units held by accounts with a nonzero delegate.
268
+ /// @param tierId The tier to get the delegated voting units of.
269
+ /// @param blockNumber The past block number to look up.
270
+ /// @return activeVotes The tier's delegated voting units at `blockNumber`.
271
+ function getPastTotalTierActiveVotes(
272
+ uint256 tierId,
273
+ uint256 blockNumber
274
+ )
275
+ external
276
+ view
277
+ override
278
+ returns (uint256 activeVotes)
279
+ {
280
+ // forge-lint: disable-next-line(unsafe-typecast)
281
+ activeVotes = _tierActiveSupplyCheckpointsOf[tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
190
282
  }
191
283
 
192
284
  /// @notice The current total delegated voting units.
@@ -196,23 +288,29 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
196
288
  activeVotes = _activeSupplyCheckpoints.latest();
197
289
  }
198
290
 
291
+ /// @notice The current total delegated voting units of a tier.
292
+ /// @param tierId The tier to get the current delegated voting units of.
293
+ /// @return activeVotes The tier's current delegated voting units.
294
+ function getTotalTierActiveVotes(uint256 tierId) external view override returns (uint256 activeVotes) {
295
+ activeVotes = _tierActiveSupplyCheckpointsOf[tierId].latest();
296
+ }
297
+
199
298
  /// @notice The owner of an NFT at a past block.
200
- /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
201
- /// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
299
+ /// @dev Returns `address(0)` if no ownership checkpoint exists or the query predates the first checkpoint.
202
300
  /// @param tokenId The token ID of the NFT to get the historical owner of.
203
301
  /// @param blockNumber The block number to look up.
204
- /// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
205
- function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
302
+ /// @return owner The owner of the token at `blockNumber`, or zero if no owner is proven at that block.
303
+ function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address owner) {
206
304
  // forge-lint: disable-next-line(unsafe-typecast)
207
305
  uint96 blockNumber96 = uint96(blockNumber);
208
306
 
209
307
  Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
210
308
  uint256 checkpointCount = checkpoints.length();
211
309
 
212
- // No checkpoints = not enrolled and never transferred. Not eligible.
310
+ // No checkpoints means no historical owner can be proven for this token.
213
311
  if (checkpointCount == 0) return address(0);
214
312
 
215
- // Query is before the first checkpoint token not yet enrolled/transferred at this block.
313
+ // Query is before the first checkpoint, so this token had no proven owner at that block.
216
314
  if (checkpoints.at(0)._key > blockNumber96) return address(0);
217
315
 
218
316
  return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
@@ -242,9 +340,15 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
242
340
  if (oldDelegate == address(0) && delegatee != address(0)) {
243
341
  // Add the account's voting units to the checkpointed active total.
244
342
  _updateActiveVotes({amount: votingUnits, increase: true});
343
+
344
+ // Add the account's voting units to each tier-level active total it currently contributes to.
345
+ _applyAccountDelegationToTierActiveVotes({account: account, increase: true});
245
346
  } else if (oldDelegate != address(0) && delegatee == address(0)) {
246
347
  // If the account had a delegate and now has none, its voting units just became inactive.
247
348
  _updateActiveVotes({amount: votingUnits, increase: false});
349
+
350
+ // Remove the account's voting units from each tier-level active total it currently contributes to.
351
+ _applyAccountDelegationToTierActiveVotes({account: account, increase: false});
248
352
  }
249
353
  }
250
354
 
@@ -279,8 +383,8 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
279
383
  /// @notice Returns the total voting units held by an account (across all tiers).
280
384
  /// @dev Called by OZ Votes when re-delegating to compute the account's total voting units.
281
385
  /// @param account The address to get the voting units of.
282
- /// @return The total voting units the account holds.
283
- function _getVotingUnits(address account) internal view override returns (uint256) {
386
+ /// @return votingUnits The total voting units the account holds.
387
+ function _getVotingUnits(address account) internal view override returns (uint256 votingUnits) {
284
388
  return STORE.votingUnitsOf({hook: hook, account: account});
285
389
  }
286
390
 
@@ -288,6 +392,74 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
288
392
  // ------------------------ private helpers -------------------------- //
289
393
  //*********************************************************************//
290
394
 
395
+ /// @notice Add or remove units from an account's active-voting-units checkpoint for a tier at the current block.
396
+ /// @param account The account whose active-voting-units trace should be updated.
397
+ /// @param tierId The tier whose active-voting-units trace should be updated.
398
+ /// @param amount The voting units to add or remove.
399
+ /// @param increase Whether to add `amount`; if false, `amount` is removed.
400
+ function _adjustAccountTierActiveVotes(address account, uint256 tierId, uint256 amount, bool increase) private {
401
+ // Ignore zero-unit updates because they do not change the account's active tier total.
402
+ if (amount == 0) return;
403
+
404
+ // Keep a reference to the account's active-voting-units trace for this tier.
405
+ Checkpoints.Trace160 storage trace = _accountTierActiveVotesOf[account][tierId];
406
+
407
+ // Calculate the next account-tier active total from its latest checkpointed value.
408
+ uint256 updated = increase ? trace.latest() + amount : trace.latest() - amount;
409
+
410
+ // Write the new account-tier active total at the current block.
411
+ // forge-lint: disable-next-line(unsafe-typecast)
412
+ trace.push({key: uint96(block.number), value: uint160(updated)});
413
+ }
414
+
415
+ /// @notice Add or remove units from a tier's active-voting-units checkpoint at the current block.
416
+ /// @param tierId The tier whose active-voting-units trace to update.
417
+ /// @param amount The voting units to add or remove.
418
+ /// @param increase Whether to add `amount`; if false, `amount` is removed.
419
+ function _adjustTotalTierActiveVotes(uint256 tierId, uint256 amount, bool increase) private {
420
+ // Ignore zero-unit updates because they do not change this tier's active total.
421
+ if (amount == 0) return;
422
+
423
+ // Keep a reference to the tier's active-voting-units trace.
424
+ Checkpoints.Trace160 storage trace = _tierActiveSupplyCheckpointsOf[tierId];
425
+
426
+ // Calculate the next tier active total by adding or subtracting from the latest checkpointed value.
427
+ uint256 updated = increase ? trace.latest() + amount : trace.latest() - amount;
428
+
429
+ // Write the new tier active total at the current block.
430
+ // forge-lint: disable-next-line(unsafe-typecast)
431
+ trace.push({key: uint96(block.number), value: uint160(updated)});
432
+ }
433
+
434
+ /// @notice Apply an account delegation change to every tier-level active total the account contributes to.
435
+ /// @dev Delegation is account-wide in OZ Votes, so changing an account's delegate activates or deactivates every
436
+ /// tier balance currently held by the account. Transfer hooks handle one-token tier deltas separately.
437
+ /// @param account The account whose tier voting units should be added or removed.
438
+ /// @param increase Whether to add `account`'s tier voting units; if false, they are removed.
439
+ function _applyAccountDelegationToTierActiveVotes(address account, bool increase) private {
440
+ // Read the largest tier ID once; tier IDs are sequential and 1-indexed for each hook.
441
+ uint256 tierId = STORE.maxTierIdOf(hook);
442
+
443
+ // Walk each tier from max to 1 so empty hooks skip cleanly when maxTierId is zero.
444
+ while (tierId != 0) {
445
+ // Read only this account's units for the tier; empty tiers return zero and are ignored below.
446
+ uint256 tierVotingUnits = STORE.tierVotingUnitsOf({hook: hook, account: account, tierId: tierId});
447
+
448
+ // Only write checkpoints for tiers whose active totals actually change.
449
+ if (tierVotingUnits != 0) {
450
+ _adjustTotalTierActiveVotes({tierId: tierId, amount: tierVotingUnits, increase: increase});
451
+ _adjustAccountTierActiveVotes({
452
+ account: account, tierId: tierId, amount: tierVotingUnits, increase: increase
453
+ });
454
+ }
455
+
456
+ unchecked {
457
+ // The loop condition proves tierId is nonzero, so the decrement cannot underflow.
458
+ --tierId;
459
+ }
460
+ }
461
+ }
462
+
291
463
  /// @notice Update the checkpointed total of delegated voting units.
292
464
  /// @dev Writes at most one active-total checkpoint at the current OZ clock. A zero amount is ignored so zero-value
293
465
  /// delegation or transfer hooks do not create empty checkpoints.
@@ -305,13 +477,21 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
305
477
  _activeSupplyCheckpoints.push({key: clock(), value: SafeCast.toUint208(updated)});
306
478
  }
307
479
 
308
- /// @notice Add or remove units from a tier's eligible-voting-units checkpoint at the current block.
309
- /// @param tierId The tier whose eligible-voting-units trace to update.
480
+ /// @notice Add or remove units from a tier's owner-tracked voting-units checkpoint at the current block.
481
+ /// @param tierId The tier whose owner-tracked voting-units trace to update.
310
482
  /// @param amount The voting units to add or remove.
311
483
  /// @param increase Whether to add `amount`; if false, `amount` is removed.
312
484
  function _updateTierEligibleUnits(uint256 tierId, uint256 amount, bool increase) private {
485
+ // Ignore zero-unit updates because they do not change this tier's owner-tracked total.
486
+ if (amount == 0) return;
487
+
488
+ // Keep a reference to the tier's owner-tracked voting-units trace.
313
489
  Checkpoints.Trace160 storage trace = _tierEligibleUnitsOf[tierId];
490
+
491
+ // Calculate the next owner-tracked total by adding or subtracting from the latest checkpointed value.
314
492
  uint256 updated = increase ? trace.latest() + amount : trace.latest() - amount;
493
+
494
+ // Write the new owner-tracked total at the current block.
315
495
  // forge-lint: disable-next-line(unsafe-typecast)
316
496
  trace.push({key: uint96(block.number), value: uint160(updated)});
317
497
  }
@@ -244,8 +244,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
244
244
  /// @notice Initialize a cloned copy of the hook. Sets the project association, ERC-721 name/symbol, pricing
245
245
  /// context (currency + decimals), metadata URIs, initial tiers, and behavioral flags. Can only be called once
246
246
  /// per clone — the implementation contract is pre-initialized in its constructor to prevent misuse.
247
- /// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts if called more than once or if the
248
- /// project ID is zero.
247
+ /// @dev Called by `JB721TiersHookDeployer` after cloning. Reverts if called twice or if the project ID is zero.
249
248
  /// @param initialProjectId The ID of the project this hook is associated with.
250
249
  /// @param name The name of the NFT collection.
251
250
  /// @param symbol The symbol representing the NFT collection.
@@ -461,9 +460,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
461
460
  /// @param symbol The new collection symbol. Send empty to leave unchanged.
462
461
  /// @param baseUri The new base URI. Send empty to leave unchanged.
463
462
  /// @param contractUri The new contract URI. Send empty to leave unchanged.
464
- /// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value
465
- /// to leave unchanged. `address(this)` is used instead of `address(0)` because `address(0)` is a valid value that
466
- /// clears the resolver.
463
+ /// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` to leave it
464
+ /// unchanged; `address(0)` clears the resolver.
467
465
  /// @param encodedIpfsUriTierId The ID of the tier to set the encoded IPFS URI of.
468
466
  /// @param encodedIpfsUri The encoded IPFS URI to set.
469
467
  function setMetadata(
@@ -114,8 +114,7 @@ contract JB721TiersHookProjectDeployer is
114
114
  }
115
115
 
116
116
  /// @notice Launches rulesets for a project with an attached 721 tiers hook.
117
- /// @dev Only a project's owner or an operator with the `LAUNCH_RULESETS & SET_TERMINALS` permission can launch its
118
- /// rulesets.
117
+ /// @dev Only a project's owner or an operator with `LAUNCH_RULESETS & SET_TERMINALS` can launch its rulesets.
119
118
  /// @param projectId The ID of the project to launch rulesets for.
120
119
  /// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook to deploy.
121
120
  /// @param launchRulesetsConfig Configuration which dictates the project's new rulesets.
@@ -233,8 +232,7 @@ contract JB721TiersHookProjectDeployer is
233
232
  //*********************************************************************//
234
233
 
235
234
  /// @notice Configure and launch rulesets for a newly created project. Converts `JBPayDataHookRulesetConfig` entries
236
- /// into standard `JBRulesetConfig` entries with `useDataHookForPay` forced to `true` and the deployed hook set as
237
- /// the data hook.
235
+ /// into standard `JBRulesetConfig` entries that use the deployed hook as their data hook.
238
236
  /// @param projectId The ID of the reserved project.
239
237
  /// @param launchProjectConfig Configuration which dictates the behavior of the project to launch.
240
238
  /// @param dataHook The data hook to use for the project.
@@ -148,8 +148,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
148
148
  /// @custom:param hook The 721 contract to get the custom token URI resolver of.
149
149
  mapping(address hook => IJB721TokenUriResolver) public override tokenUriResolverOf;
150
150
 
151
- /// @notice The combined cash-out weight of all of a hook's NFTs, as a running aggregate so cash-out pricing is
152
- /// O(1) instead of O(maxTierId).
151
+ /// @notice The combined cash-out weight of all hook NFTs, tracked so cash-out pricing is O(1).
153
152
  /// @dev Maintained incrementally in `recordMint` (+ the tier's full price for the new outstanding NFT plus any
154
153
  /// newly-accrued pending reserve) and `recordBurn` (- the tier's full price). It is invariant under everything
155
154
  /// else: reserve mints are weight-neutral (a pending reserve becomes an outstanding NFT), removed tiers keep
@@ -610,8 +609,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
610
609
  /// @param hook The 721 contract to get the tier from.
611
610
  /// @param tierId The ID of the tier to get.
612
611
  /// @param storedTier The stored tier to get the corresponding tier for.
613
- /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
614
- /// resolved and included.
612
+ /// @param includeResolvedUri If true and the contract has a token URI resolver, resolve and include its content.
615
613
  /// @return tier The tier as a `JB721Tier` struct.
616
614
  function _getTierFrom(
617
615
  address hook,
@@ -135,8 +135,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
135
135
  cashOutTaxRate = context.cashOutTaxRate;
136
136
  }
137
137
 
138
- /// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
139
- /// terminal's `pay(...)` transaction.
138
+ /// @notice The data calculated before a payment is recorded in the terminal store for `pay(...)`.
140
139
  /// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
141
140
  /// @param context The payment context passed to this contract by the `pay(...)` function.
142
141
  /// @return weight The new `weight` to use, overriding the ruleset's `weight`.
@@ -164,8 +163,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
164
163
  // -------------------------- public views --------------------------- //
165
164
  //*********************************************************************//
166
165
 
167
- /// @notice Returns the cumulative cash out weight of the specified token IDs relative to the
168
- /// `totalCashOutWeight`.
166
+ /// @notice Returns the cumulative cash out weight of the specified token IDs relative to `totalCashOutWeight`.
169
167
  /// @param tokenIds The NFT token IDs to calculate the cumulative cash out weight of.
170
168
  /// @return The cumulative cash out weight of the specified token IDs.
171
169
  function cashOutWeightOf(uint256[] memory tokenIds) public view virtual returns (uint256) {
@@ -248,8 +246,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
248
246
  _didBurn(decodedTokenIds);
249
247
  }
250
248
 
251
- /// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
252
- /// `IJBPayHook`.
249
+ /// @notice Mints NFTs to `context.beneficiary` upon payment if conditions are met. Part of `IJBPayHook`.
253
250
  /// @dev Reverts if the calling contract is not one of the project's terminals.
254
251
  /// @param context The payment context passed in by the terminal.
255
252
  function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
@@ -3,6 +3,7 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  import {IJBActiveVotes} from "@bananapus/core-v6/src/interfaces/IJBActiveVotes.sol";
5
5
  import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol";
6
+
6
7
  import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
7
8
 
8
9
  /// @notice A checkpoint module that provides IVotes-compatible checkpointed voting power for a JB721TiersHook.
@@ -10,38 +11,67 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
10
11
  /// Pass this address to JBTokenDistributor as the IVotes token.
11
12
  interface IJB721Checkpoints is IERC5805, IJBActiveVotes {
12
13
  /// @notice The store that holds tier and voting data for the hook's NFTs.
13
- /// @return The store contract.
14
+ /// @return store The store contract.
14
15
  // forge-lint: disable-next-line(mixed-case-function)
15
- function STORE() external view returns (IJB721TiersHookStore);
16
+ function STORE() external view returns (IJB721TiersHookStore store);
17
+
18
+ /// @notice The delegated voting units held by an account in a tier at a past block.
19
+ /// @dev Counts only tier voting units held by `account` while `account` had a nonzero delegate.
20
+ /// @param account The account to get the delegated tier voting units of.
21
+ /// @param tierId The tier to get the delegated voting units of.
22
+ /// @param blockNumber The past block number to look up.
23
+ /// @return activeVotes The account's delegated tier voting units at `blockNumber`.
24
+ function getPastAccountTierActiveVotes(
25
+ address account,
26
+ uint256 tierId,
27
+ uint256 blockNumber
28
+ )
29
+ external
30
+ view
31
+ returns (uint256 activeVotes);
16
32
 
17
- /// @notice The total eligible voting units of a tier at a past block.
18
- /// @dev "Eligible" means a token has an owner checkpoint: it was enrolled via `delegate(address,uint256[])` or
19
- /// transferred at least once. Minted-but-unenrolled tokens are excluded, mirroring `ownerOfAt` eligibility, and
20
- /// mints never write to this trace. Distributors use this as the denominator for tier-scoped reward pots.
21
- /// @param tierId The tier to get the eligible voting units of.
33
+ /// @notice The total owner-checkpointed voting units of a tier at a past block.
34
+ /// @dev Owner-checkpointed voting units are the tier's total owned units, regardless of delegation status.
35
+ /// @param tierId The tier to get the owner-checkpointed voting units of.
22
36
  /// @param blockNumber The block number to look up (must be strictly in the past).
23
- /// @return The tier's eligible voting units at `blockNumber`.
24
- function getPastTierVotingUnits(uint256 tierId, uint256 blockNumber) external view returns (uint256);
37
+ /// @return votingUnits The tier's owner-checkpointed voting units at `blockNumber`.
38
+ function getPastTierVotingUnits(uint256 tierId, uint256 blockNumber) external view returns (uint256 votingUnits);
39
+
40
+ /// @notice The total delegated voting units of a tier at a past block.
41
+ /// @dev Counts only tier voting units held by accounts with a nonzero delegate.
42
+ /// @param tierId The tier to get the delegated voting units of.
43
+ /// @param blockNumber The past block number to look up.
44
+ /// @return activeVotes The tier's delegated voting units at `blockNumber`.
45
+ function getPastTotalTierActiveVotes(
46
+ uint256 tierId,
47
+ uint256 blockNumber
48
+ )
49
+ external
50
+ view
51
+ returns (uint256 activeVotes);
52
+
53
+ /// @notice The current total delegated voting units of a tier.
54
+ /// @param tierId The tier to get the current delegated voting units of.
55
+ /// @return activeVotes The tier's current delegated voting units.
56
+ function getTotalTierActiveVotes(uint256 tierId) external view returns (uint256 activeVotes);
25
57
 
26
58
  /// @notice The hook that this module tracks voting power for.
27
- /// @return The hook address.
59
+ /// @return hookAddress The hook address.
28
60
  // forge-lint: disable-next-line(mixed-case-function)
29
- function hook() external view returns (address);
61
+ function hook() external view returns (address hookAddress);
30
62
 
31
63
  /// @notice The owner of an NFT at a past block.
32
- /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
33
- /// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
64
+ /// @dev Returns `address(0)` if no ownership checkpoint exists or the query predates the first checkpoint.
34
65
  /// @param tokenId The token ID of the NFT to get the historical owner of.
35
66
  /// @param blockNumber The block number to look up.
36
- /// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
37
- function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address);
67
+ /// @return owner The owner of the token at `blockNumber`, or zero if no owner is proven at that block.
68
+ function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address owner);
38
69
 
39
- /// @notice Delegates voting power and enrolls tokens for distribution eligibility.
40
- /// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
41
- /// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
42
- /// distribution.
70
+ /// @notice Delegates voting power and backfills ownership history for listed tokens if needed.
71
+ /// @dev Mint and transfer hooks normally write owner checkpoints automatically. The token ID list keeps
72
+ /// pre-upgrade or otherwise uncheckpointed tokens recoverable while preserving the owner-only authorization check.
43
73
  /// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
44
- /// @param tokenIds The token IDs to enroll for distribution eligibility.
74
+ /// @param tokenIds The token IDs whose owner checkpoints should be backfilled if missing.
45
75
  function delegate(address delegatee, uint256[] calldata tokenIds) external;
46
76
 
47
77
  /// @notice Initializes a cloned module with its hook reference.
@@ -96,8 +96,7 @@ interface IJB721TiersHook is IJB721Hook {
96
96
  /// @param caller The address that called the function.
97
97
  event SetTokenUriResolver(IJB721TokenUriResolver indexed resolver, address caller);
98
98
 
99
- /// @notice Emitted when a split payout reverts during distribution. The failed split's funds route to the
100
- /// project's balance.
99
+ /// @notice Emitted when a split payout reverts during distribution, routing funds to the project's balance.
101
100
  /// @param projectId The project ID the split belongs to.
102
101
  /// @param split The split that reverted.
103
102
  /// @param amount The amount that was paid out.
@@ -213,9 +212,8 @@ interface IJB721TiersHook is IJB721Hook {
213
212
  /// @param symbol The new collection symbol. Send empty to leave unchanged.
214
213
  /// @param baseUri The new base URI. Send empty to leave unchanged.
215
214
  /// @param contractUri The new contract URI. Send empty to leave unchanged.
216
- /// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value
217
- /// to leave unchanged. `address(this)` is used instead of `address(0)` because `address(0)` is a valid value that
218
- /// clears the resolver.
215
+ /// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` to leave it
216
+ /// unchanged; `address(0)` clears the resolver.
219
217
  /// @param encodedIpfsUriTierId The ID of the tier to set the encoded IPFS URI of.
220
218
  /// @param encodedIpfsUri The encoded IPFS URI to set.
221
219
  function setMetadata(
@@ -73,8 +73,7 @@ library JB721TiersHookLib {
73
73
  /// @param caller The address that called the function.
74
74
  event SetDiscountPercent(uint256 indexed tierId, uint256 discountPercent, address caller);
75
75
 
76
- /// @notice Emitted when a split payout reverts during distribution. The failed split's funds route to the
77
- /// project's balance.
76
+ /// @notice Emitted when a split payout reverts during distribution, routing funds to the project's balance.
78
77
  /// @param projectId The project ID the split belongs to.
79
78
  /// @param split The split that reverted.
80
79
  /// @param amount The amount that was paid out.
@@ -1,8 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- /// @notice `JB721TiersHook` options which are packed and stored in the corresponding `JBRulesetMetadata.metadata` on a
5
- /// per-ruleset basis.
4
+ /// @notice `JB721TiersHook` options packed into each ruleset's `JBRulesetMetadata.metadata`.
6
5
  /// @custom:member pauseTransfers A boolean indicating whether NFT transfers are paused during this ruleset.
7
6
  /// @custom:member pauseMintPendingReserves A boolean indicating whether pending/outstanding NFT reserves can be minted
8
7
  /// during this ruleset.
@@ -5,8 +5,7 @@ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.
5
5
 
6
6
  import {JBPayDataHookRulesetConfig} from "./JBPayDataHookRulesetConfig.sol";
7
7
 
8
- /// @custom:member projectUri Metadata URI to associate with the project. This can be updated any time by the owner of
9
- /// the project.
8
+ /// @custom:member projectUri Metadata URI to associate with the project, updatable by the project owner.
10
9
  /// @custom:member rulesetConfigurations The ruleset configurations to queue.
11
10
  /// @custom:member terminalConfigurations The terminal configurations to add for the project.
12
11
  /// @custom:member memo A memo to pass along to the emitted event.
@@ -14,17 +14,14 @@ import {JBPayDataHookRulesetMetadata} from "./JBPayDataHookRulesetMetadata.sol";
14
14
  /// a project owner cannot make changes to a ruleset's parameters while it is active – any proposed changes will apply
15
15
  /// to the subsequent ruleset. If no changes are proposed, a ruleset rolls over to another one with the same properties
16
16
  /// but new `start` timestamp and a decayed `weight`.
17
- /// @custom:member weight A fixed point number with 18 decimals that contracts can use to base arbitrary calculations
18
- /// on. For example, payment terminals can use this to determine how many tokens should be minted when a payment is
19
- /// received.
20
- /// @custom:member weightCutPercent A percent by how much the `weight` of the subsequent ruleset should be reduced, if
21
- /// the
22
- /// project owner hasn't queued the subsequent ruleset with an explicit `weight`. If it's 0, each ruleset will have
23
- /// equal weight. If the number is 90%, the next ruleset will have a 10% smaller weight. This weight is out of
17
+ /// @custom:member weight A fixed point number with 18 decimals that contracts can use for calculations. For example,
18
+ /// payment terminals can use this to determine how many tokens should be minted when a payment is received.
19
+ /// @custom:member weightCutPercent A percent by how much the `weight` of the subsequent ruleset should be reduced if
20
+ /// the project owner hasn't queued the subsequent ruleset with an explicit `weight`. If it's 0, each ruleset will
21
+ /// have equal weight. If the number is 90%, the next ruleset will have a 10% smaller weight. This weight is out of
24
22
  /// `JBConstants.MAX_WEIGHT_CUT_PERCENT`.
25
- /// @custom:member approvalHook An address of a contract that says whether a proposed ruleset should be accepted or
26
- /// rejected. It
27
- /// can be used to create rules around how a project owner can change ruleset parameters over time.
23
+ /// @custom:member approvalHook A contract that says whether a proposed ruleset should be accepted or rejected. It can
24
+ /// create rules around how a project owner changes ruleset parameters over time.
28
25
  /// @custom:member metadata Metadata specifying the controller-specific parameters that a ruleset can have. These
29
26
  /// properties cannot change until the next ruleset starts.
30
27
  /// @custom:member splitGroups An array of splits to use for any number of groups while the ruleset is active.
@@ -13,12 +13,10 @@ pragma solidity ^0.8.0;
13
13
  /// permission from the owner should be allowed to mint project tokens on demand during this ruleset.
14
14
  /// @custom:member allowSetCustomToken A flag indicating if the project owner can set the project's token to a custom
15
15
  /// ERC-20.
16
- /// @custom:member allowTerminalMigration A flag indicating if migrating terminals should be allowed during this
17
- /// ruleset.
16
+ /// @custom:member allowTerminalMigration A flag indicating if terminal migration is allowed during this ruleset.
18
17
  /// @custom:member allowSetTerminals A flag indicating if a project's terminals can be added or removed.
19
18
  /// @custom:member allowSetController A flag indicating if a project's controller can be changed.
20
- /// @custom:member allowAddAccountingContext A flag indicating if a project can add new accounting contexts for its
21
- /// terminals to use.
19
+ /// @custom:member allowAddAccountingContext A flag indicating if a project can add accounting contexts to terminals.
22
20
  /// @custom:member allowAddPriceFeed A flag indicating if a project can add new price feeds to calculate exchange rates
23
21
  /// between its tokens.
24
22
  /// @custom:member ownerMustSendPayouts A flag indicating if the owner must manually trigger payout distributions.