@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.
@@ -27,9 +27,10 @@ import {JBVestingLoan} from "./structs/JBVestingLoan.sol";
27
27
  /// to stakers with linear vesting. Each round, a snapshot is taken of the distributable balance, and stakers can
28
28
  /// claim their pro-rata share based on their stake weight at the snapshot block. Claimed tokens vest linearly over
29
29
  /// `VESTING_ROUNDS` rounds and can be collected as they unlock.
30
- /// @dev Subclasses define how stake is measured (`_tokenStake`, `_totalStake`), who can claim (`_canClaim`), and
31
- /// what "burned" means (`_tokenBurned`). Two concrete implementations exist: `JBTokenDistributor` (IVotes tokens)
32
- /// and `JB721Distributor` (Juicebox 721 NFTs).
30
+ /// @dev Subclasses define how stake is measured (`_tokenStake`, `_totalStake`), who can redirect collected rewards
31
+ /// (`_claimBeneficiaryOf`, `_canClaim`), how token IDs are validated (`_validateTokenIds`), and what "burned" means
32
+ /// (`_tokenBurned`). Two concrete implementations exist: `JBTokenDistributor` (IVotes tokens) and `JB721Distributor`
33
+ /// (Juicebox 721 NFTs).
33
34
  abstract contract JBDistributor is IJBDistributor {
34
35
  using SafeERC20 for IERC20;
35
36
 
@@ -222,6 +223,7 @@ abstract contract JBDistributor is IJBDistributor {
222
223
  // -------------------------- constructor ---------------------------- //
223
224
  //*********************************************************************//
224
225
 
226
+ /// @notice Initializes the shared distributor configuration.
225
227
  /// @param controller The JB controller used for token registry lookups and revnet loan permissions.
226
228
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
227
229
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
@@ -266,10 +268,10 @@ abstract contract JBDistributor is IJBDistributor {
266
268
  //*********************************************************************//
267
269
 
268
270
  /// @notice Begin vesting all unclaimed past reward rounds for the specified token IDs.
269
- /// @dev Materializes each token ID's pro-rata share of every past (non-current) reward round into fresh vesting
271
+ /// @dev Permissionless. Materializes each token ID's pro-rata share of every past reward round into fresh vesting
270
272
  /// entries that start now and unlock over `VESTING_ROUNDS`. Current-round funding is excluded until a later round
271
- /// starts. The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
272
- /// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
273
+ /// starts. The model-specific per-round claim math and token ID validation live in the `_claimPastRewards` and
274
+ /// `_validateTokenIds` hooks each concrete distributor implements.
273
275
  /// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
274
276
  /// @param tokenIds The staker token IDs to claim rewards for.
275
277
  /// @param tokens The reward tokens to begin vesting.
@@ -296,13 +298,19 @@ abstract contract JBDistributor is IJBDistributor {
296
298
  _fund({hook: hook, groupId: 0, token: token, amount: amount});
297
299
  }
298
300
 
301
+ /// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
302
+ /// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
303
+ function poke() external override {
304
+ _ensureSnapshotBlock(currentRound());
305
+ }
306
+
299
307
  /// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
300
308
  /// @dev Recycling is permissionless; any keeper or frontend can sweep an expired round.
301
309
  /// @param hook The hook whose expired rewards should be recycled.
302
310
  /// @param token The reward token to recycle.
303
311
  /// @param rounds The reward rounds to recycle.
304
312
  /// @return amount The total amount recycled.
305
- function burnExpiredRewards(
313
+ function recycleExpiredRewards(
306
314
  address hook,
307
315
  IERC20 token,
308
316
  uint256[] calldata rounds
@@ -312,17 +320,12 @@ abstract contract JBDistributor is IJBDistributor {
312
320
  override
313
321
  returns (uint256 amount)
314
322
  {
315
- amount = _burnExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
316
- }
317
-
318
- /// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
319
- /// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
320
- function poke() external override {
321
- _ensureSnapshotBlock(currentRound());
323
+ amount = _recycleExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
322
324
  }
323
325
 
324
- /// @notice Recycle unlocked rewards tied to burned tokens into the current reward round.
325
- /// @dev Anyone can call this for burned tokens.
326
+ /// @notice Recycle rewards tied to burned tokens into the current reward round as they unlock.
327
+ /// @dev Anyone can call this for burned tokens. Unclaimed historical shares are materialized before unlocked
328
+ /// forfeited amounts are recycled.
326
329
  /// @param hook The hook whose tokens were burned.
327
330
  /// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
328
331
  /// @param tokens The reward tokens to recycle.
@@ -413,27 +416,6 @@ abstract contract JBDistributor is IJBDistributor {
413
416
  // ----------------------- public transactions ----------------------- //
414
417
  //*********************************************************************//
415
418
 
416
- /// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
417
- /// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
418
- /// @dev The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
419
- /// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
420
- /// @param hook The hook whose stakers are collecting.
421
- /// @param tokenIds The IDs of the tokens to collect for (caller must be authorized for all of them).
422
- /// @param tokens The reward tokens to collect vested amounts of.
423
- /// @param beneficiary The recipient of the collected tokens.
424
- function collectVestedRewards(
425
- address hook,
426
- uint256[] calldata tokenIds,
427
- IERC20[] calldata tokens,
428
- address beneficiary
429
- )
430
- public
431
- virtual
432
- override
433
- {
434
- _collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
435
- }
436
-
437
419
  /// @notice Borrow from a revnet using one token ID's uncollected vesting rewards as collateral.
438
420
  /// @dev The distributor keeps custody of the loan NFT. Collection is blocked until repayment restores the
439
421
  /// collateral to the original vesting schedule.
@@ -472,6 +454,27 @@ abstract contract JBDistributor is IJBDistributor {
472
454
  });
473
455
  }
474
456
 
457
+ /// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
458
+ /// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
459
+ /// @dev Authorized holders can collect to any beneficiary. Helpers can collect only to the canonical beneficiary
460
+ /// for every token ID they do not control.
461
+ /// @param hook The hook whose stakers are collecting.
462
+ /// @param tokenIds The IDs of the tokens to collect for.
463
+ /// @param tokens The reward tokens to collect vested amounts of.
464
+ /// @param beneficiary The recipient of the collected tokens.
465
+ function collectVestedRewards(
466
+ address hook,
467
+ uint256[] calldata tokenIds,
468
+ IERC20[] calldata tokens,
469
+ address beneficiary
470
+ )
471
+ public
472
+ virtual
473
+ override
474
+ {
475
+ _collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
476
+ }
477
+
475
478
  /// @notice Repay a distributor-held Revnet loan and restore its collateral to the original vesting schedule.
476
479
  /// @param loanId The Revnet loan NFT ID to repay.
477
480
  /// @param maxRepayBorrowAmount The maximum source-token amount the caller is willing to repay.
@@ -576,11 +579,6 @@ abstract contract JBDistributor is IJBDistributor {
576
579
  internal
577
580
  virtual;
578
581
 
579
- /// @notice Revert unless the caller is authorized to claim each token ID.
580
- /// @param hook The hook whose token IDs are being checked.
581
- /// @param tokenIds The token IDs to check.
582
- function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
583
-
584
582
  /// @notice Shared begin-vesting logic across reward groups.
585
583
  /// @param hook The hook whose stakers are vesting.
586
584
  /// @param groupId The reward group (0 = the default group).
@@ -600,8 +598,8 @@ abstract contract JBDistributor is IJBDistributor {
600
598
  // Revert if no token IDs are provided.
601
599
  if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
602
600
 
603
- // Only the entity authorized for these token IDs may start their vesting clock.
604
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
601
+ // Validate token IDs before a permissionless helper can materialize vesting state.
602
+ _validateTokenIds({hook: hook, tokenIds: tokenIds});
605
603
 
606
604
  // Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
607
605
  _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
@@ -628,8 +626,11 @@ abstract contract JBDistributor is IJBDistributor {
628
626
  // Revert if no token IDs are provided.
629
627
  if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
630
628
 
631
- // Only the entity authorized for these token IDs may materialize and collect their rewards.
632
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
629
+ // Validate token IDs before a permissionless helper can materialize vesting state.
630
+ _validateTokenIds({hook: hook, tokenIds: tokenIds});
631
+
632
+ // Only authorized holders can redirect rewards; helpers must send them to the canonical beneficiary.
633
+ _requireCanCollectTo({hook: hook, tokenIds: tokenIds, beneficiary: beneficiary});
633
634
 
634
635
  // Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
635
636
  _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
@@ -641,6 +642,8 @@ abstract contract JBDistributor is IJBDistributor {
641
642
  }
642
643
 
643
644
  /// @notice Shared forfeiture-release logic across reward groups.
645
+ /// @dev Materializes unclaimed historical shares for burned token IDs before recycling the currently unlocked
646
+ /// forfeited amount.
644
647
  /// @param hook The hook whose tokens were burned.
645
648
  /// @param groupId The reward group (0 = the default group).
646
649
  /// @param tokenIds The IDs of the burned tokens.
@@ -658,6 +661,9 @@ abstract contract JBDistributor is IJBDistributor {
658
661
  // Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
659
662
  _requireNotAcceptingToken();
660
663
 
664
+ // Let concrete distributors enforce forfeiture-only validation before claim cursors can move.
665
+ _validateForfeitedTokenIds({hook: hook, tokenIds: tokenIds});
666
+
661
667
  // Make sure that all staker token IDs are burned.
662
668
  for (uint256 i; i < tokenIds.length;) {
663
669
  if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
@@ -668,7 +674,10 @@ abstract contract JBDistributor is IJBDistributor {
668
674
  }
669
675
  }
670
676
 
671
- // Unlock the rewards and recycle the forfeited amount.
677
+ // Materialize any still-unclaimed historical shares using the same reward math as live claims.
678
+ _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
679
+
680
+ // Unlock the vested forfeiture amount and recycle it into the current reward round.
672
681
  _unlockRewards({
673
682
  hook: hook,
674
683
  groupId: groupId,
@@ -685,7 +694,7 @@ abstract contract JBDistributor is IJBDistributor {
685
694
  /// @param token The reward token to recycle.
686
695
  /// @param rounds The reward rounds to recycle.
687
696
  /// @return amount The total amount recycled.
688
- function _burnExpiredRewards(
697
+ function _recycleExpiredRewards(
689
698
  address hook,
690
699
  uint256 groupId,
691
700
  IERC20 token,
@@ -748,7 +757,7 @@ abstract contract JBDistributor is IJBDistributor {
748
757
  // Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
749
758
  if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
750
759
 
751
- // Make sure that all tokens can be claimed by this sender.
760
+ // Only the authorized holder can collateralize vesting rewards and choose the loan beneficiary.
752
761
  _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
753
762
 
754
763
  // Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
@@ -1548,6 +1557,41 @@ abstract contract JBDistributor is IJBDistributor {
1548
1557
  /// @return canClaim True if the account can collect rewards for this token ID.
1549
1558
  function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
1550
1559
 
1560
+ /// @notice The canonical beneficiary for permissionless collection of a token ID's rewards.
1561
+ /// @param hook The hook the token ID belongs to.
1562
+ /// @param tokenId The token ID to get the claim beneficiary of.
1563
+ /// @return beneficiary The address that helpers can collect the token ID's rewards to.
1564
+ function _claimBeneficiaryOf(address hook, uint256 tokenId) internal view virtual returns (address beneficiary);
1565
+
1566
+ /// @notice Revert unless the caller can collect the requested token IDs to the beneficiary.
1567
+ /// @dev A caller that controls a token ID can route that token ID's collected rewards anywhere. A helper that
1568
+ /// does not control the token ID can only collect to that token ID's canonical beneficiary.
1569
+ /// @param hook The hook the token IDs belong to.
1570
+ /// @param tokenIds The token IDs whose collected rewards will be transferred.
1571
+ /// @param beneficiary The address that will receive the collected rewards.
1572
+ function _requireCanCollectTo(address hook, uint256[] calldata tokenIds, address beneficiary) internal view {
1573
+ for (uint256 i; i < tokenIds.length;) {
1574
+ uint256 tokenId = tokenIds[i];
1575
+
1576
+ // Holders can choose any beneficiary for token IDs they control.
1577
+ if (!_canClaim({hook: hook, tokenId: tokenId, account: msg.sender})) {
1578
+ // Helpers can only send rewards to the token ID's canonical beneficiary.
1579
+ if (beneficiary != _claimBeneficiaryOf({hook: hook, tokenId: tokenId})) {
1580
+ revert JBDistributor_NoAccess({hook: hook, tokenId: tokenId, account: msg.sender});
1581
+ }
1582
+ }
1583
+
1584
+ unchecked {
1585
+ ++i;
1586
+ }
1587
+ }
1588
+ }
1589
+
1590
+ /// @notice Revert unless the caller is authorized to redirect rewards or borrow against each token ID.
1591
+ /// @param hook The hook whose token IDs are being checked.
1592
+ /// @param tokenIds The token IDs to check.
1593
+ function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
1594
+
1551
1595
  /// @notice Revert if called while an inbound ERC-20 transfer is being measured.
1552
1596
  /// @dev Reward tokens are arbitrary contracts. This guard prevents token callbacks from mutating distributor
1553
1597
  /// accounting midway through a balance-delta measurement.
@@ -1570,13 +1614,23 @@ abstract contract JBDistributor is IJBDistributor {
1570
1614
  }
1571
1615
  }
1572
1616
 
1573
- /// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations,
1574
- /// and their unlocked forfeited rewards can be recycled via `releaseForfeitedRewards`.
1617
+ /// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations, and
1618
+ /// their historical forfeited rewards can be materialized and recycled via `releaseForfeitedRewards`.
1575
1619
  /// @param hook The hook the token belongs to.
1576
1620
  /// @param tokenId The token ID to check.
1577
1621
  /// @return tokenWasBurned True if the token has been burned.
1578
1622
  function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
1579
1623
 
1624
+ /// @notice Validate token IDs passed to `releaseForfeitedRewards`.
1625
+ /// @dev Defaults to no additional validation. Concrete distributors can enforce ordering or model-specific rules
1626
+ /// that are not captured by `_tokenBurned`.
1627
+ /// @param hook The hook the token IDs belong to.
1628
+ /// @param tokenIds The token IDs to validate for forfeiture.
1629
+ function _validateForfeitedTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual {
1630
+ hook;
1631
+ tokenIds;
1632
+ }
1633
+
1580
1634
  /// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
1581
1635
  /// @dev Subclasses define how stake is measured.
1582
1636
  /// @param hook The hook the token belongs to.
@@ -1600,4 +1654,9 @@ abstract contract JBDistributor is IJBDistributor {
1600
1654
  view
1601
1655
  virtual
1602
1656
  returns (uint256 totalStakedAmount);
1657
+
1658
+ /// @notice Revert unless each token ID is valid for this concrete distributor.
1659
+ /// @param hook The hook the token IDs belong to.
1660
+ /// @param tokenIds The token IDs to validate.
1661
+ function _validateTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
1603
1662
  }