@bananapus/721-hook-v6 0.0.72 → 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
@@ -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 owner-tracked voting units (`getPastTierVotingUnits`), global active vote totals (`getPastTotalActiveVotes`), and per-tier active vote totals (`getPastTierActiveVotes`). |
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
 
@@ -65,7 +65,8 @@ If a bug affects supply, reserve minting, or tier lookup, it usually lives in th
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
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 `getPastTierActiveVotes(tierId, blockNumber)` / `getTierActiveVotes(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.
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.72",
3
+ "version": "0.0.73",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
 
@@ -55,6 +55,13 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
55
55
  // -------------------- internal stored properties ------------------- //
56
56
  //*********************************************************************//
57
57
 
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
+
58
65
  /// @notice Checkpointed token owners for historical reward eligibility.
59
66
  /// @dev Mint and transfer hooks write this automatically; `delegate` only backfills missing pre-upgrade history.
60
67
  /// @custom:param tokenId The token ID to get historical owner checkpoints for.
@@ -189,10 +196,20 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
189
196
  // Move checkpointed voting power from the previous owner to the new owner.
190
197
  _transferVotingUnits({from: from, to: to, amount: votingUnits});
191
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
+
192
209
  // If both sides are delegated or both sides are undelegated, this tier's active total is unchanged.
193
210
  if (decreaseTierActiveVotes != increaseTierActiveVotes) {
194
211
  // Otherwise apply the one-sided active-tier delta implied by the receiver's delegated status.
195
- _adjustTierActiveVotes({tierId: tierId, amount: votingUnits, increase: increaseTierActiveVotes});
212
+ _adjustTotalTierActiveVotes({tierId: tierId, amount: votingUnits, increase: increaseTierActiveVotes});
196
213
  }
197
214
  }
198
215
 
@@ -200,12 +217,14 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
200
217
  // ----------------------- external views ---------------------------- //
201
218
  //*********************************************************************//
202
219
 
203
- /// @notice The total delegated voting units of a tier at a past block.
204
- /// @dev Counts only tier voting units held by accounts with a nonzero delegate.
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.
205
223
  /// @param tierId The tier to get the delegated voting units of.
206
224
  /// @param blockNumber The past block number to look up.
207
- /// @return activeVotes The tier's delegated voting units at `blockNumber`.
208
- function getPastTierActiveVotes(
225
+ /// @return activeVotes The account's delegated tier voting units at `blockNumber`.
226
+ function getPastAccountTierActiveVotes(
227
+ address account,
209
228
  uint256 tierId,
210
229
  uint256 blockNumber
211
230
  )
@@ -215,7 +234,8 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
215
234
  returns (uint256 activeVotes)
216
235
  {
217
236
  // forge-lint: disable-next-line(unsafe-typecast)
218
- activeVotes = _tierActiveSupplyCheckpointsOf[tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
237
+ activeVotes =
238
+ _accountTierActiveVotesOf[account][tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
219
239
  }
220
240
 
221
241
  /// @notice The total owner-checkpointed voting units of a tier at a past block.
@@ -243,11 +263,22 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
243
263
  activeVotes = _activeSupplyCheckpoints.upperLookupRecent(_validateTimepoint(blockNumber));
244
264
  }
245
265
 
246
- /// @notice The current total delegated voting units of a tier.
247
- /// @param tierId The tier to get the current delegated voting units of.
248
- /// @return activeVotes The tier's current delegated voting units.
249
- function getTierActiveVotes(uint256 tierId) external view override returns (uint256 activeVotes) {
250
- activeVotes = _tierActiveSupplyCheckpointsOf[tierId].latest();
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)));
251
282
  }
252
283
 
253
284
  /// @notice The current total delegated voting units.
@@ -257,6 +288,13 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
257
288
  activeVotes = _activeSupplyCheckpoints.latest();
258
289
  }
259
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
+
260
298
  /// @notice The owner of an NFT at a past block.
261
299
  /// @dev Returns `address(0)` if no ownership checkpoint exists or the query predates the first checkpoint.
262
300
  /// @param tokenId The token ID of the NFT to get the historical owner of.
@@ -354,11 +392,31 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
354
392
  // ------------------------ private helpers -------------------------- //
355
393
  //*********************************************************************//
356
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
+
357
415
  /// @notice Add or remove units from a tier's active-voting-units checkpoint at the current block.
358
416
  /// @param tierId The tier whose active-voting-units trace to update.
359
417
  /// @param amount The voting units to add or remove.
360
418
  /// @param increase Whether to add `amount`; if false, `amount` is removed.
361
- function _adjustTierActiveVotes(uint256 tierId, uint256 amount, bool increase) private {
419
+ function _adjustTotalTierActiveVotes(uint256 tierId, uint256 amount, bool increase) private {
362
420
  // Ignore zero-unit updates because they do not change this tier's active total.
363
421
  if (amount == 0) return;
364
422
 
@@ -389,7 +447,10 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
389
447
 
390
448
  // Only write checkpoints for tiers whose active totals actually change.
391
449
  if (tierVotingUnits != 0) {
392
- _adjustTierActiveVotes({tierId: tierId, amount: tierVotingUnits, increase: increase});
450
+ _adjustTotalTierActiveVotes({tierId: tierId, amount: tierVotingUnits, increase: increase});
451
+ _adjustAccountTierActiveVotes({
452
+ account: account, tierId: tierId, amount: tierVotingUnits, increase: increase
453
+ });
393
454
  }
394
455
 
395
456
  unchecked {
@@ -15,12 +15,20 @@ interface IJB721Checkpoints is IERC5805, IJBActiveVotes {
15
15
  // forge-lint: disable-next-line(mixed-case-function)
16
16
  function STORE() external view returns (IJB721TiersHookStore store);
17
17
 
18
- /// @notice The total delegated voting units of a tier at a past block.
19
- /// @dev Counts only tier voting units held by accounts with a nonzero delegate.
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.
20
21
  /// @param tierId The tier to get the delegated voting units of.
21
22
  /// @param blockNumber The past block number to look up.
22
- /// @return activeVotes The tier's delegated voting units at `blockNumber`.
23
- function getPastTierActiveVotes(uint256 tierId, uint256 blockNumber) external view returns (uint256 activeVotes);
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);
24
32
 
25
33
  /// @notice The total owner-checkpointed voting units of a tier at a past block.
26
34
  /// @dev Owner-checkpointed voting units are the tier's total owned units, regardless of delegation status.
@@ -29,10 +37,23 @@ interface IJB721Checkpoints is IERC5805, IJBActiveVotes {
29
37
  /// @return votingUnits The tier's owner-checkpointed voting units at `blockNumber`.
30
38
  function getPastTierVotingUnits(uint256 tierId, uint256 blockNumber) external view returns (uint256 votingUnits);
31
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
+
32
53
  /// @notice The current total delegated voting units of a tier.
33
54
  /// @param tierId The tier to get the current delegated voting units of.
34
55
  /// @return activeVotes The tier's current delegated voting units.
35
- function getTierActiveVotes(uint256 tierId) external view returns (uint256 activeVotes);
56
+ function getTotalTierActiveVotes(uint256 tierId) external view returns (uint256 activeVotes);
36
57
 
37
58
  /// @notice The hook that this module tracks voting power for.
38
59
  /// @return hookAddress The hook address.