@bananapus/distributor-v6 0.0.38 → 0.0.43

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.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
+ import {IJBActiveVotes} from "@bananapus/core-v6/src/interfaces/IJBActiveVotes.sol";
4
5
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
5
6
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
7
  import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
@@ -15,27 +16,26 @@ import {IREVLoans} from "@rev-net/core-v6/src/interfaces/IREVLoans.sol";
15
16
  import {IREVOwner} from "@rev-net/core-v6/src/interfaces/IREVOwner.sol";
16
17
 
17
18
  import {JBDistributor} from "./JBDistributor.sol";
18
- import {IJBDistributor} from "./interfaces/IJBDistributor.sol";
19
19
  import {IJBTokenDistributor} from "./interfaces/IJBTokenDistributor.sol";
20
20
  import {JBClaimContext} from "./structs/JBClaimContext.sol";
21
21
  import {JBRewardRoundData} from "./structs/JBRewardRoundData.sol";
22
22
  import {JBVestingData} from "./structs/JBVestingData.sol";
23
23
 
24
- /// @notice A singleton distributor that distributes ERC-20 rewards to IVotes-compatible token stakers with linear
25
- /// vesting.
24
+ /// @notice A singleton distributor that distributes ERC-20 rewards to IVotes-compatible token holders with delegated
25
+ /// voting power and linear vesting.
26
26
  /// @dev Any project can use this distributor by configuring a payout split with
27
27
  /// `hook = this contract` and `beneficiary = address(their IVotes token)`.
28
- /// @dev The stake weight of each staker is their delegated voting power at the funded round's snapshot block.
28
+ /// @dev The stake weight of each holder is their delegated voting power at the funded round's snapshot block.
29
29
  /// Holders must delegate (even to themselves) to participate.
30
- /// @dev Funded rewards are assigned to the funding round. Stakers claim historical rounds lazily; all unclaimed past
31
- /// rewards begin vesting when the staker claims, not when the rewards were funded.
30
+ /// @dev Funded rewards are assigned to the funding round. Holders claim historical rounds lazily; all unclaimed past
31
+ /// rewards begin vesting when the holder claims, not when the rewards were funded.
32
32
  /// @dev Implements `IJBSplitHook` so it can receive tokens directly from Juicebox project payout splits.
33
33
  contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
34
34
  //*********************************************************************//
35
35
  // --------------------------- custom errors ------------------------- //
36
36
  //*********************************************************************//
37
37
 
38
- /// @notice Thrown when a tokenId has non-zero upper bits (above 160), which would alias to the same staker address.
38
+ /// @notice Thrown when a tokenId has non-zero upper bits (above 160), which would alias to the same holder address.
39
39
  error JBTokenDistributor_InvalidTokenId(uint256 tokenId);
40
40
 
41
41
  /// @notice Thrown when native ETH does not match the split hook context amount.
@@ -52,7 +52,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
52
52
  //*********************************************************************//
53
53
 
54
54
  /// @notice The JB directory used to verify terminal/controller callers.
55
- IJBDirectory public immutable DIRECTORY;
55
+ IJBDirectory public immutable override DIRECTORY;
56
56
 
57
57
  //*********************************************************************//
58
58
  // --------------------- public stored properties -------------------- //
@@ -71,6 +71,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
71
71
  // -------------------------- constructor ---------------------------- //
72
72
  //*********************************************************************//
73
73
 
74
+ /// @notice Initializes the token distributor.
74
75
  /// @param directory The JB directory used to verify terminal/controller callers.
75
76
  /// @param controller The JB controller used for token registry lookups and revnet loan permissions.
76
77
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
@@ -93,7 +94,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
93
94
  }
94
95
 
95
96
  //*********************************************************************//
96
- // ---------------------- receive ----------------------------------- //
97
+ // ------------------------- receive / fallback ---------------------- //
97
98
  //*********************************************************************//
98
99
 
99
100
  /// @notice Allows the contract to receive native ETH (e.g. from payout splits).
@@ -148,9 +149,9 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
148
149
  }
149
150
 
150
151
  // `beginVesting` and `collectVestedRewards` are provided by `JBDistributor`. Both distributors share the exact
151
- // same flow (authorize -> materialize past rounds via `_claimPastRewards` -> optionally release unlocked), so the
152
+ // same flow (validate -> materialize past rounds via `_claimPastRewards` -> optionally release unlocked), so the
152
153
  // round-claim logic lives once in the base and dispatches to this contract's `_claimPastRewards` /
153
- // `_requireCanClaimTokenIds` overrides below.
154
+ // `_validateTokenIds` overrides below.
154
155
 
155
156
  //*********************************************************************//
156
157
  // -------------------------- public views --------------------------- //
@@ -170,6 +171,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
170
171
 
171
172
  /// @notice Claim all past reward rounds for the given token IDs and reward tokens into fresh vesting entries.
172
173
  /// @param hook The IVotes token whose stakers are claiming.
174
+ /// @param groupId The reward group being claimed.
173
175
  /// @param tokenIds The encoded staker addresses to claim for.
174
176
  /// @param tokens The reward tokens to claim.
175
177
  function _claimPastRewards(
@@ -241,8 +243,9 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
241
243
  // If the cursor is already past the last completed round, this staker is current.
242
244
  if (nextClaimRound > ctx.lastClaimableRound) return 0;
243
245
 
244
- // Sum this staker's pro-rata share from every unclaimed completed reward round.
245
- tokenAmount = _claimRewardsFor({
246
+ // Sum this staker's pro-rata share from every resolved completed reward round.
247
+ uint256 newNextClaimRound;
248
+ (tokenAmount, newNextClaimRound) = _claimRewardsFor({
246
249
  hook: ctx.hook,
247
250
  groupId: ctx.groupId,
248
251
  tokenId: tokenId,
@@ -251,8 +254,10 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
251
254
  lastRound: ctx.lastClaimableRound
252
255
  });
253
256
 
254
- // Advance the cursor even when the amount is zero, so empty or zero-stake rounds are not rescanned forever.
255
- nextClaimRoundOf[ctx.hook][ctx.groupId][tokenId][token] = ctx.lastClaimableRound + 1;
257
+ // Advance the cursor through resolved rounds.
258
+ nextClaimRoundOf[ctx.hook][ctx.groupId][tokenId][token] = newNextClaimRound;
259
+
260
+ // Avoid writing empty vesting entries when no past round allocates rewards to this staker.
256
261
  if (tokenAmount == 0) return 0;
257
262
 
258
263
  // All accumulated past rewards start a single fresh vesting schedule at the claim round.
@@ -260,6 +265,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
260
265
  JBVestingData({releaseRound: ctx.vestingReleaseRound, amount: tokenAmount, shareClaimed: 0})
261
266
  );
262
267
 
268
+ // Emit once per staker and reward token so offchain systems can track the materialized vesting entry.
263
269
  emit Claimed({
264
270
  hook: ctx.hook,
265
271
  tokenId: tokenId,
@@ -271,17 +277,50 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
271
277
  });
272
278
  }
273
279
 
274
- //*********************************************************************//
275
- // ----------------------- internal views ---------------------------- //
276
- //*********************************************************************//
280
+ /// @notice Claim one reward round using its recorded denominator.
281
+ /// @param hook The IVotes token whose stakers are claiming.
282
+ /// @param tokenId The encoded staker address.
283
+ /// @param rewardRound The stored reward-round data.
284
+ /// @return tokenAmount The amount added to vesting.
285
+ function _claimRewardRoundFor(
286
+ address hook,
287
+ uint256 tokenId,
288
+ JBRewardRoundData storage rewardRound
289
+ )
290
+ internal
291
+ returns (uint256 tokenAmount)
292
+ {
293
+ // Empty-denominator rounds have no pro-rata basis, so they cannot allocate rewards to any staker.
294
+ if (rewardRound.totalStake == 0) return 0;
295
+
296
+ // Use the funding round's snapshot block, not the block at which the staker finally claims.
297
+ uint256 tokenStakeAmount = _tokenStakeAt({hook: hook, tokenId: tokenId, blockNumber: rewardRound.snapshotBlock});
298
+
299
+ // Zero-vote stakers advance their cursor but do not consume reward inventory.
300
+ if (tokenStakeAmount == 0) return 0;
301
+
302
+ // The round's reward pot is split pro-rata across checkpointed voting power.
303
+ uint256 claimAmount = mulDiv({x: rewardRound.amount, y: tokenStakeAmount, denominator: rewardRound.totalStake});
304
+
305
+ // Ignore floor-rounded zero claims to avoid unnecessary storage writes.
306
+ if (claimAmount == 0) return 0;
307
+
308
+ // Track the portion that has started vesting so expiry recycles only the remainder.
309
+ rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + claimAmount);
310
+
311
+ // Return the exact amount that the caller should append to the staker's vesting entry.
312
+ tokenAmount = claimAmount;
313
+ }
277
314
 
278
315
  /// @notice Claim a staker's unclaimed rewards across a range of historical reward rounds.
279
316
  /// @param hook The IVotes token whose stakers are claiming.
317
+ /// @param groupId The reward group being claimed.
280
318
  /// @param tokenId The encoded staker address.
281
319
  /// @param token The reward token.
282
320
  /// @param firstRound The first reward round to include.
283
321
  /// @param lastRound The last reward round to include.
284
322
  /// @return tokenAmount The cumulative unclaimed reward amount.
323
+ /// @return newNextClaimRound The next reward round this staker has not yet resolved.
285
324
  function _claimRewardsFor(
286
325
  address hook,
287
326
  uint256 groupId,
@@ -291,8 +330,10 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
291
330
  uint256 lastRound
292
331
  )
293
332
  internal
294
- returns (uint256 tokenAmount)
333
+ returns (uint256 tokenAmount, uint256 newNextClaimRound)
295
334
  {
335
+ newNextClaimRound = lastRound + 1;
336
+
296
337
  // Walk every unclaimed historical round. The caller bounds this to completed rounds only.
297
338
  for (uint256 rewardRoundNumber = firstRound; rewardRoundNumber <= lastRound;) {
298
339
  // Load this round's reward data for the hook, group, and reward token.
@@ -300,29 +341,12 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
300
341
 
301
342
  // Skip rounds that never received funding.
302
343
  if (rewardRound.amount != 0) {
303
- // Expired rounds can no longer be claimed as-is; recycle their unclaimed remainder instead.
344
+ // Expired rounds forfeit unmaterialized inventory into the current active-voter set.
304
345
  if (_rewardRoundExpired(rewardRound)) {
305
346
  _recycleExpiredRewardRound({hook: hook, groupId: groupId, token: token, round: rewardRoundNumber});
306
- } else if (rewardRound.totalStake != 0) {
307
- // Use the funding round's snapshot block, not the block at which the staker finally claims.
308
- uint256 tokenStakeAmount =
309
- _tokenStakeAt({hook: hook, tokenId: tokenId, blockNumber: rewardRound.snapshotBlock});
310
-
311
- // Zero-vote stakers advance their cursor but do not consume reward inventory.
312
- if (tokenStakeAmount != 0) {
313
- // The round's reward pot is split pro-rata across checkpointed voting power.
314
- uint256 claimAmount =
315
- mulDiv({x: rewardRound.amount, y: tokenStakeAmount, denominator: rewardRound.totalStake});
316
-
317
- // Ignore floor-rounded zero claims to avoid unnecessary storage writes.
318
- if (claimAmount != 0) {
319
- // Track the portion that has started vesting so expiry recycles only the remainder.
320
- rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + claimAmount);
321
-
322
- // Add this round's vested amount to the staker's cumulative claim.
323
- tokenAmount += claimAmount;
324
- }
325
- }
347
+ } else {
348
+ // Live rounds can still be materialized by snapshot voters into fresh vesting entries.
349
+ tokenAmount += _claimRewardRoundFor({hook: hook, tokenId: tokenId, rewardRound: rewardRound});
326
350
  }
327
351
  }
328
352
 
@@ -332,18 +356,48 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
332
356
  }
333
357
  }
334
358
 
335
- /// @notice Check if the account matches the staker address encoded in the tokenId.
336
- /// @dev tokenId encodes the staker address as `uint256(uint160(stakerAddress))`.
359
+ //*********************************************************************//
360
+ // ----------------------- internal views ---------------------------- //
361
+ //*********************************************************************//
362
+
363
+ /// @notice Check if the account matches the staker address encoded in the token ID.
364
+ /// @dev The token ID encodes the staker address as `uint256(uint160(stakerAddress))`.
337
365
  /// @param hook Unused — access is determined by the tokenId encoding.
338
366
  /// @param tokenId The encoded staker address.
339
367
  /// @param account The account to check.
340
368
  /// @return canClaim True if the account matches the encoded address.
341
369
  function _canClaim(address hook, uint256 tokenId, address account) internal pure override returns (bool canClaim) {
370
+ hook; // Silence unused variable warning.
371
+ canClaim = _claimBeneficiaryOf({hook: hook, tokenId: tokenId}) == account;
372
+ }
373
+
374
+ /// @notice The encoded staker address that receives permissionless collections.
375
+ /// @dev Reverts on high-bit aliasing so every token ID maps to exactly one address.
376
+ /// @param hook Unused — the beneficiary is determined by the token ID encoding.
377
+ /// @param tokenId The encoded staker address.
378
+ /// @return beneficiary The staker address encoded in `tokenId`.
379
+ function _claimBeneficiaryOf(address hook, uint256 tokenId) internal pure override returns (address beneficiary) {
342
380
  hook; // Silence unused variable warning.
343
381
  if (tokenId >> 160 != 0) revert JBTokenDistributor_InvalidTokenId({tokenId: tokenId});
344
382
  // The high bits were checked above, so this cast recovers the encoded address.
345
383
  // forge-lint: disable-next-line(unsafe-typecast)
346
- canClaim = address(uint160(tokenId)) == account;
384
+ beneficiary = address(uint160(tokenId));
385
+ }
386
+
387
+ /// @notice Revert unless the caller is authorized to claim each token ID.
388
+ /// @param hook The IVotes token whose stakers are claiming.
389
+ /// @param tokenIds The encoded staker addresses to check.
390
+ function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view override {
391
+ // Each tokenId is an encoded address, so every requested claim must belong to msg.sender.
392
+ for (uint256 i; i < tokenIds.length;) {
393
+ if (!_canClaim({hook: hook, tokenId: tokenIds[i], account: msg.sender})) {
394
+ revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
395
+ }
396
+
397
+ unchecked {
398
+ ++i;
399
+ }
400
+ }
347
401
  }
348
402
 
349
403
  /// @notice IVotes tokens cannot be "burned" in the NFT sense — always returns false.
@@ -359,8 +413,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
359
413
 
360
414
  /// @notice The delegated voting power of a staker at the current round's snapshot block.
361
415
  /// @dev Uses `IVotes.getPastVotes` for checkpointed lookups. The block number is derived from
362
- /// `roundSnapshotBlock[currentRound()]`, which is set on first interaction in a round and
363
- /// consistent with the block used for `_totalStake` in `beginVesting` and `collectVestedRewards`.
416
+ /// `roundSnapshotBlock[currentRound()]`, which is set on first interaction in a round.
364
417
  /// @param hook The IVotes-compatible token contract.
365
418
  /// @param tokenId The encoded staker address (`uint256(uint160(stakerAddress))`).
366
419
  /// @return tokenStakeAmount The delegated voting power at the round's snapshot block.
@@ -369,43 +422,6 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
369
422
  _tokenStakeAt({hook: hook, tokenId: tokenId, blockNumber: roundSnapshotBlock[currentRound()]});
370
423
  }
371
424
 
372
- /// @notice The total supply of votes at a specific block.
373
- /// @dev Uses `IVotes.getPastTotalSupply` for checkpointed lookups. Token distributors have no tier concept, so the
374
- /// group only isolates reward storage and does not change the stake denominator.
375
- /// @param hook The IVotes-compatible token contract.
376
- /// @param groupId The reward group (unused for token distributors — kept for base-hook conformance).
377
- /// @param blockNumber The block number to get the total supply at.
378
- /// @return totalStakedAmount The total supply of votes at the given block.
379
- function _totalStake(
380
- address hook,
381
- uint256 groupId,
382
- uint256 blockNumber
383
- )
384
- internal
385
- view
386
- override
387
- returns (uint256 totalStakedAmount)
388
- {
389
- groupId; // Silence unused variable warning — token distributors are group-agnostic in weight.
390
- totalStakedAmount = IVotes(hook).getPastTotalSupply(blockNumber);
391
- }
392
-
393
- /// @notice Revert unless the caller is authorized to claim each token ID.
394
- /// @param hook The IVotes token whose stakers are claiming.
395
- /// @param tokenIds The encoded staker addresses to check.
396
- function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view override {
397
- // Each tokenId is an encoded address, so every requested claim must belong to msg.sender.
398
- for (uint256 i; i < tokenIds.length;) {
399
- if (!_canClaim({hook: hook, tokenId: tokenIds[i], account: msg.sender})) {
400
- revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
401
- }
402
-
403
- unchecked {
404
- ++i;
405
- }
406
- }
407
- }
408
-
409
425
  /// @notice The delegated voting power of a staker at an explicit snapshot block.
410
426
  /// @param hook The IVotes-compatible token contract.
411
427
  /// @param tokenId The encoded staker address.
@@ -430,4 +446,41 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
430
446
  // Query the staker's delegated votes at the reward round's fixed snapshot block.
431
447
  tokenStakeAmount = IVotes(hook).getPastVotes({account: account, timepoint: blockNumber});
432
448
  }
449
+
450
+ /// @notice The total stake denominator recorded when a token reward round is first funded.
451
+ /// @dev Always uses `IJBActiveVotes.getPastTotalActiveVotes`, so undelegated balances such as AMM-held tokens do
452
+ /// not share rewards. `CLAIM_DURATION` only controls whether unmaterialized allocations can expire.
453
+ /// @param hook The IVotes-compatible token contract.
454
+ /// @param groupId The reward group (unused for token distributors — kept for base-hook conformance).
455
+ /// @param blockNumber The block number to get the active total at.
456
+ /// @return totalStakedAmount The stake denominator to record for the funded round.
457
+ function _totalStake(
458
+ address hook,
459
+ uint256 groupId,
460
+ uint256 blockNumber
461
+ )
462
+ internal
463
+ view
464
+ override
465
+ returns (uint256 totalStakedAmount)
466
+ {
467
+ groupId; // Silence unused variable warning — token distributors are group-agnostic in weight.
468
+
469
+ // All token reward rounds split only across units delegated to nonzero delegates at the snapshot block.
470
+ totalStakedAmount = IJBActiveVotes(hook).getPastTotalActiveVotes(blockNumber);
471
+ }
472
+
473
+ /// @notice Revert unless every token ID can be decoded as a staker address.
474
+ /// @param hook The IVotes token whose staker slots are being validated.
475
+ /// @param tokenIds The encoded staker addresses to validate.
476
+ function _validateTokenIds(address hook, uint256[] calldata tokenIds) internal pure override {
477
+ // Permissionless helpers can start vesting for any valid encoded staker slot.
478
+ for (uint256 i; i < tokenIds.length;) {
479
+ _claimBeneficiaryOf({hook: hook, tokenId: tokenIds[i]});
480
+
481
+ unchecked {
482
+ ++i;
483
+ }
484
+ }
485
+ }
433
486
  }
@@ -18,13 +18,15 @@ interface IJB721Distributor is IJBDistributor, IJBSplitHook {
18
18
  //*********************************************************************//
19
19
 
20
20
  /// @notice The JB directory used to verify terminal/controller callers.
21
- function DIRECTORY() external view returns (IJBDirectory);
21
+ /// @return directory The JB directory.
22
+ function DIRECTORY() external view returns (IJBDirectory directory);
22
23
 
23
24
  /// @notice Calculate how much of the token has been claimed for the given tokenId in a tier-scoped group.
24
25
  /// @param hook The hook the tokenId belongs to.
25
26
  /// @param tierIds The strictly-increasing tier set defining the group.
26
27
  /// @param tokenId The ID of the token to calculate the token amount for.
27
28
  /// @param token The address of the token to check.
29
+ /// @return tokenAmount The claimed token amount.
28
30
  function claimedFor(
29
31
  address hook,
30
32
  uint256[] calldata tierIds,
@@ -33,14 +35,14 @@ interface IJB721Distributor is IJBDistributor, IJBSplitHook {
33
35
  )
34
36
  external
35
37
  view
36
- returns (uint256);
38
+ returns (uint256 tokenAmount);
37
39
 
38
- /// @notice Calculate how much of the token is currently ready to be collected for the given tokenId in a
39
- /// tier-scoped group.
40
+ /// @notice Calculate the collectible token amount for a token ID in a tier-scoped group.
40
41
  /// @param hook The hook the tokenId belongs to.
41
42
  /// @param tierIds The strictly-increasing tier set defining the group.
42
43
  /// @param tokenId The ID of the token to calculate the token amount for.
43
44
  /// @param token The address of the token to check.
45
+ /// @return tokenAmount The currently collectable token amount.
44
46
  function collectableFor(
45
47
  address hook,
46
48
  uint256[] calldata tierIds,
@@ -49,7 +51,7 @@ interface IJB721Distributor is IJBDistributor, IJBSplitHook {
49
51
  )
50
52
  external
51
53
  view
52
- returns (uint256);
54
+ returns (uint256 tokenAmount);
53
55
 
54
56
  /// @notice The tier set that defines a reward group, recorded when the group is first funded.
55
57
  /// @dev Empty for the all-tiers group (0).
@@ -63,6 +65,7 @@ interface IJB721Distributor is IJBDistributor, IJBSplitHook {
63
65
  //*********************************************************************//
64
66
 
65
67
  /// @notice Claims tokens and begins vesting from a tier-scoped reward group.
68
+ /// @dev Permissionless. No reward tokens leave the distributor.
66
69
  /// @param hook The hook whose stakers are vesting.
67
70
  /// @param tierIds The strictly-increasing tier set defining the group.
68
71
  /// @param tokenIds The IDs to claim rewards for.
@@ -99,22 +102,9 @@ interface IJB721Distributor is IJBDistributor, IJBSplitHook {
99
102
  external
100
103
  returns (uint256 loanId, uint256 collateralCount);
101
104
 
102
- /// @notice Recycle unclaimed rewards from expired tier-scoped reward rounds into the current reward round.
103
- /// @param hook The hook whose expired reward rounds should be recycled.
104
- /// @param tierIds The strictly-increasing tier set defining the group.
105
- /// @param token The reward token to recycle.
106
- /// @param rounds The reward rounds to recycle.
107
- /// @return amount The total amount recycled.
108
- function burnExpiredRewards(
109
- address hook,
110
- uint256[] calldata tierIds,
111
- IERC20 token,
112
- uint256[] calldata rounds
113
- )
114
- external
115
- returns (uint256 amount);
116
-
117
105
  /// @notice Collect vested tokens from a tier-scoped reward group.
106
+ /// @dev Authorized NFT owners can collect to any beneficiary. Helpers can collect only to the canonical
107
+ /// beneficiary of every token ID they do not control.
118
108
  /// @param hook The hook whose stakers are collecting.
119
109
  /// @param tierIds The strictly-increasing tier set defining the group.
120
110
  /// @param tokenIds The IDs of the tokens to collect for.
@@ -137,7 +127,23 @@ interface IJB721Distributor is IJBDistributor, IJBSplitHook {
137
127
  /// @param amount The amount to fund.
138
128
  function fund(address hook, uint256[] calldata tierIds, IERC20 token, uint256 amount) external payable;
139
129
 
140
- /// @notice Recycle unlocked rewards from burned tokens in a tier-scoped group into the current reward round.
130
+ /// @notice Recycle unclaimed rewards from expired tier-scoped reward rounds into the current reward round.
131
+ /// @param hook The hook whose expired reward rounds should be recycled.
132
+ /// @param tierIds The strictly-increasing tier set defining the group.
133
+ /// @param token The reward token to recycle.
134
+ /// @param rounds The reward rounds to recycle.
135
+ /// @return amount The total amount recycled.
136
+ function recycleExpiredRewards(
137
+ address hook,
138
+ uint256[] calldata tierIds,
139
+ IERC20 token,
140
+ uint256[] calldata rounds
141
+ )
142
+ external
143
+ returns (uint256 amount);
144
+
145
+ /// @notice Recycle rewards from burned tokens in a tier-scoped group into the current reward round as they unlock.
146
+ /// @dev Unclaimed historical reward shares are materialized before the unlocked forfeited amount is recycled.
141
147
  /// @param hook The hook whose tokens were burned.
142
148
  /// @param tierIds The strictly-increasing tier set defining the group.
143
149
  /// @param tokenIds The IDs of the burned tokens.