@bananapus/distributor-v6 0.0.37 → 0.0.42

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
 
@@ -163,6 +164,7 @@ abstract contract JBDistributor is IJBDistributor {
163
164
 
164
165
  /// @notice The block number recorded as the snapshot point for each round.
165
166
  /// @dev Set to `block.number - 1` on first interaction in a round, so that `IVotes.getPastVotes` works.
167
+ /// @custom:param round The round whose snapshot block is being recorded.
166
168
  mapping(uint256 round => uint256) public override roundSnapshotBlock;
167
169
 
168
170
  /// @notice Reward data assigned to each funding round.
@@ -221,6 +223,7 @@ abstract contract JBDistributor is IJBDistributor {
221
223
  // -------------------------- constructor ---------------------------- //
222
224
  //*********************************************************************//
223
225
 
226
+ /// @notice Initializes the shared distributor configuration.
224
227
  /// @param controller The JB controller used for token registry lookups and revnet loan permissions.
225
228
  /// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
226
229
  /// @param revOwner The REVOwner contract that must own revnet reward token projects.
@@ -265,10 +268,10 @@ abstract contract JBDistributor is IJBDistributor {
265
268
  //*********************************************************************//
266
269
 
267
270
  /// @notice Begin vesting all unclaimed past reward rounds for the specified token IDs.
268
- /// @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
269
272
  /// entries that start now and unlock over `VESTING_ROUNDS`. Current-round funding is excluded until a later round
270
- /// starts. The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
271
- /// 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.
272
275
  /// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
273
276
  /// @param tokenIds The staker token IDs to claim rewards for.
274
277
  /// @param tokens The reward tokens to begin vesting.
@@ -295,13 +298,19 @@ abstract contract JBDistributor is IJBDistributor {
295
298
  _fund({hook: hook, groupId: 0, token: token, amount: amount});
296
299
  }
297
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
+
298
307
  /// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
299
- /// @dev The selector name is kept for compatibility with existing keeper integrations.
308
+ /// @dev Recycling is permissionless; any keeper or frontend can sweep an expired round.
300
309
  /// @param hook The hook whose expired rewards should be recycled.
301
310
  /// @param token The reward token to recycle.
302
311
  /// @param rounds The reward rounds to recycle.
303
312
  /// @return amount The total amount recycled.
304
- function burnExpiredRewards(
313
+ function recycleExpiredRewards(
305
314
  address hook,
306
315
  IERC20 token,
307
316
  uint256[] calldata rounds
@@ -311,17 +320,12 @@ abstract contract JBDistributor is IJBDistributor {
311
320
  override
312
321
  returns (uint256 amount)
313
322
  {
314
- amount = _burnExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
315
- }
316
-
317
- /// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
318
- /// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
319
- function poke() external override {
320
- _ensureSnapshotBlock(currentRound());
323
+ amount = _recycleExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
321
324
  }
322
325
 
323
- /// @notice Recycle unlocked rewards tied to burned tokens into the current reward round.
324
- /// @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.
325
329
  /// @param hook The hook whose tokens were burned.
326
330
  /// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
327
331
  /// @param tokens The reward tokens to recycle.
@@ -412,27 +416,6 @@ abstract contract JBDistributor is IJBDistributor {
412
416
  // ----------------------- public transactions ----------------------- //
413
417
  //*********************************************************************//
414
418
 
415
- /// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
416
- /// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
417
- /// @dev The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
418
- /// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
419
- /// @param hook The hook whose stakers are collecting.
420
- /// @param tokenIds The IDs of the tokens to collect for (caller must be authorized for all of them).
421
- /// @param tokens The reward tokens to collect vested amounts of.
422
- /// @param beneficiary The recipient of the collected tokens.
423
- function collectVestedRewards(
424
- address hook,
425
- uint256[] calldata tokenIds,
426
- IERC20[] calldata tokens,
427
- address beneficiary
428
- )
429
- public
430
- virtual
431
- override
432
- {
433
- _collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
434
- }
435
-
436
419
  /// @notice Borrow from a revnet using one token ID's uncollected vesting rewards as collateral.
437
420
  /// @dev The distributor keeps custody of the loan NFT. Collection is blocked until repayment restores the
438
421
  /// collateral to the original vesting schedule.
@@ -471,6 +454,27 @@ abstract contract JBDistributor is IJBDistributor {
471
454
  });
472
455
  }
473
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
+
474
478
  /// @notice Repay a distributor-held Revnet loan and restore its collateral to the original vesting schedule.
475
479
  /// @param loanId The Revnet loan NFT ID to repay.
476
480
  /// @param maxRepayBorrowAmount The maximum source-token amount the caller is willing to repay.
@@ -575,11 +579,6 @@ abstract contract JBDistributor is IJBDistributor {
575
579
  internal
576
580
  virtual;
577
581
 
578
- /// @notice Revert unless the caller is authorized to claim each token ID.
579
- /// @param hook The hook whose token IDs are being checked.
580
- /// @param tokenIds The token IDs to check.
581
- function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
582
-
583
582
  /// @notice Shared begin-vesting logic across reward groups.
584
583
  /// @param hook The hook whose stakers are vesting.
585
584
  /// @param groupId The reward group (0 = the default group).
@@ -599,8 +598,8 @@ abstract contract JBDistributor is IJBDistributor {
599
598
  // Revert if no token IDs are provided.
600
599
  if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
601
600
 
602
- // Only the entity authorized for these token IDs may start their vesting clock.
603
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
601
+ // Validate token IDs before a permissionless helper can materialize vesting state.
602
+ _validateTokenIds({hook: hook, tokenIds: tokenIds});
604
603
 
605
604
  // Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
606
605
  _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
@@ -627,19 +626,24 @@ abstract contract JBDistributor is IJBDistributor {
627
626
  // Revert if no token IDs are provided.
628
627
  if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
629
628
 
630
- // Only the entity authorized for these token IDs may materialize and collect their rewards.
631
- _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});
632
634
 
633
635
  // Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
634
636
  _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
635
637
 
636
- // Release whatever portion of existing vesting entries has unlocked by this round.
638
+ // Release whatever portion of vesting entries has unlocked by this round.
637
639
  _unlockRewards({
638
640
  hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true
639
641
  });
640
642
  }
641
643
 
642
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.
643
647
  /// @param hook The hook whose tokens were burned.
644
648
  /// @param groupId The reward group (0 = the default group).
645
649
  /// @param tokenIds The IDs of the burned tokens.
@@ -657,6 +661,9 @@ abstract contract JBDistributor is IJBDistributor {
657
661
  // Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
658
662
  _requireNotAcceptingToken();
659
663
 
664
+ // Let concrete distributors enforce forfeiture-only validation before claim cursors can move.
665
+ _validateForfeitedTokenIds({hook: hook, tokenIds: tokenIds});
666
+
660
667
  // Make sure that all staker token IDs are burned.
661
668
  for (uint256 i; i < tokenIds.length;) {
662
669
  if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
@@ -667,7 +674,10 @@ abstract contract JBDistributor is IJBDistributor {
667
674
  }
668
675
  }
669
676
 
670
- // 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.
671
681
  _unlockRewards({
672
682
  hook: hook,
673
683
  groupId: groupId,
@@ -684,7 +694,7 @@ abstract contract JBDistributor is IJBDistributor {
684
694
  /// @param token The reward token to recycle.
685
695
  /// @param rounds The reward rounds to recycle.
686
696
  /// @return amount The total amount recycled.
687
- function _burnExpiredRewards(
697
+ function _recycleExpiredRewards(
688
698
  address hook,
689
699
  uint256 groupId,
690
700
  IERC20 token,
@@ -747,7 +757,7 @@ abstract contract JBDistributor is IJBDistributor {
747
757
  // Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
748
758
  if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
749
759
 
750
- // 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.
751
761
  _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
752
762
 
753
763
  // Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
@@ -911,7 +921,7 @@ abstract contract JBDistributor is IJBDistributor {
911
921
  revert JBDistributor_UnexpectedNativeValue({msgValue: msg.value, token: loan.sourceToken});
912
922
  }
913
923
 
914
- // Pull the exact current payoff from the caller. Existing distributor inventory must not cover a shortfall.
924
+ // Pull the exact current payoff from the caller. Distributor inventory must not cover a shortfall.
915
925
  IERC20 sourceToken = IERC20(loan.sourceToken);
916
926
  uint256 sourceBalanceBefore = sourceToken.balanceOf(address(this));
917
927
  sourceToken.safeTransferFrom({from: msg.sender, to: address(this), value: repayBorrowAmount});
@@ -1263,7 +1273,7 @@ abstract contract JBDistributor is IJBDistributor {
1263
1273
  /// @param round The reward round.
1264
1274
  /// @return claimDeadline The deadline timestamp. Zero means no expiration.
1265
1275
  function _claimDeadlineFor(uint256 round) internal view returns (uint48 claimDeadline) {
1266
- // Zero duration keeps the round non-expiring and backward compatible with existing fund paths.
1276
+ // A zero claim duration means the round never expires.
1267
1277
  if (CLAIM_DURATION == 0) return 0;
1268
1278
 
1269
1279
  // Start the window at the next round boundary, when the funded round first becomes claimable.
@@ -1547,6 +1557,41 @@ abstract contract JBDistributor is IJBDistributor {
1547
1557
  /// @return canClaim True if the account can collect rewards for this token ID.
1548
1558
  function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
1549
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
+
1550
1595
  /// @notice Revert if called while an inbound ERC-20 transfer is being measured.
1551
1596
  /// @dev Reward tokens are arbitrary contracts. This guard prevents token callbacks from mutating distributor
1552
1597
  /// accounting midway through a balance-delta measurement.
@@ -1569,13 +1614,23 @@ abstract contract JBDistributor is IJBDistributor {
1569
1614
  }
1570
1615
  }
1571
1616
 
1572
- /// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations,
1573
- /// 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`.
1574
1619
  /// @param hook The hook the token belongs to.
1575
1620
  /// @param tokenId The token ID to check.
1576
1621
  /// @return tokenWasBurned True if the token has been burned.
1577
1622
  function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
1578
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
+
1579
1634
  /// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
1580
1635
  /// @dev Subclasses define how stake is measured.
1581
1636
  /// @param hook The hook the token belongs to.
@@ -1599,4 +1654,9 @@ abstract contract JBDistributor is IJBDistributor {
1599
1654
  view
1600
1655
  virtual
1601
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;
1602
1662
  }