@bananapus/distributor-v6 0.0.34 → 0.0.35

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.
@@ -143,17 +143,23 @@ abstract contract JBDistributor is IJBDistributor {
143
143
 
144
144
  /// @notice The active Revnet loan using one token ID's vesting rewards as collateral.
145
145
  /// @custom:param hook The hook the token ID belongs to.
146
+ /// @custom:param groupId The reward group (0 = the default group).
146
147
  /// @custom:param tokenId The token ID whose vesting rewards are collateralized.
147
148
  /// @custom:param token The reward token used as loan collateral.
148
- mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256)))
149
+ mapping(
150
+ address hook => mapping(uint256 groupId => mapping(uint256 tokenId => mapping(IERC20 token => uint256)))
151
+ )
149
152
  public
150
153
  override activeVestingLoanIdOf;
151
154
 
152
155
  /// @notice The index within `vestingDataOf` of the latest vest.
153
156
  /// @custom:param hook The hook the tokenId belongs to.
157
+ /// @custom:param groupId The reward group (0 = the default group).
154
158
  /// @custom:param tokenId The ID of the token to which the vests belong.
155
159
  /// @custom:param token The address of the token vested.
156
- mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256))) public latestVestedIndexOf;
160
+ mapping(
161
+ address hook => mapping(uint256 groupId => mapping(uint256 tokenId => mapping(IERC20 token => uint256)))
162
+ ) public latestVestedIndexOf;
157
163
 
158
164
  /// @notice The block number recorded as the snapshot point for each round.
159
165
  /// @dev Set to `block.number - 1` on first interaction in a round, so that `IVotes.getPastVotes` works.
@@ -161,9 +167,12 @@ abstract contract JBDistributor is IJBDistributor {
161
167
 
162
168
  /// @notice Reward data assigned to each funding round.
163
169
  /// @custom:param hook The stake source whose stakers receive rewards.
170
+ /// @custom:param groupId The reward group (0 = the default group).
164
171
  /// @custom:param token The reward token.
165
172
  /// @custom:param round The reward round.
166
- mapping(address hook => mapping(IERC20 token => mapping(uint256 round => JBRewardRoundData))) public rewardRoundOf;
173
+ mapping(
174
+ address hook => mapping(uint256 groupId => mapping(IERC20 token => mapping(uint256 round => JBRewardRoundData)))
175
+ ) public rewardRoundOf;
167
176
 
168
177
  /// @notice The amount of a token that is currently vesting for a hook's stakers.
169
178
  /// @custom:param hook The hook whose stakers are vesting.
@@ -177,9 +186,12 @@ abstract contract JBDistributor is IJBDistributor {
177
186
 
178
187
  /// @notice All vesting data of a tokenId for any number of vesting tokens.
179
188
  /// @custom:param hook The hook the tokenId belongs to.
189
+ /// @custom:param groupId The reward group (0 = the default group).
180
190
  /// @custom:param tokenId The ID of the token to which the vests belong.
181
191
  /// @custom:param token The address of the token vested.
182
- mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => JBVestingData[]))) public vestingDataOf;
192
+ mapping(
193
+ address hook => mapping(uint256 groupId => mapping(uint256 tokenId => mapping(IERC20 token => JBVestingData[])))
194
+ ) public vestingDataOf;
183
195
 
184
196
  //*********************************************************************//
185
197
  // -------------------- internal stored properties ------------------- //
@@ -269,19 +281,7 @@ abstract contract JBDistributor is IJBDistributor {
269
281
  virtual
270
282
  override
271
283
  {
272
- // Reward accounting cannot change while an ERC-20 `transferFrom` is in progress. A callback-capable reward
273
- // token could otherwise vest or collect against balances mid-transfer, distorting the credited delta.
274
- _requireNotAcceptingToken();
275
-
276
- // Revert if no token IDs are provided.
277
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
278
-
279
- // Only the entity authorized for these token IDs (current NFT owner / encoded staker) may start their
280
- // vesting clock — third parties must not start it for them.
281
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
282
-
283
- // Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
284
- _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
284
+ _beginVesting({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens});
285
285
  }
286
286
 
287
287
  /// @notice Directly fund the distributor for a specific hook by pulling tokens from the caller. An alternative
@@ -292,7 +292,7 @@ abstract contract JBDistributor is IJBDistributor {
292
292
  /// @param token The token to fund with.
293
293
  /// @param amount The amount to fund (ignored for native ETH — `msg.value` is used instead).
294
294
  function fund(address hook, IERC20 token, uint256 amount) external payable virtual override {
295
- _fund({hook: hook, token: token, amount: amount});
295
+ _fund({hook: hook, groupId: 0, token: token, amount: amount});
296
296
  }
297
297
 
298
298
  /// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
@@ -311,19 +311,7 @@ abstract contract JBDistributor is IJBDistributor {
311
311
  override
312
312
  returns (uint256 amount)
313
313
  {
314
- // Do not let reward-token callbacks recycle inventory during an inbound balance-delta measurement.
315
- _requireNotAcceptingToken();
316
-
317
- // Process every requested round independently so callers can batch keeper work.
318
- for (uint256 i; i < rounds.length;) {
319
- // Add this round's expired remainder to the batch total.
320
- amount += _recycleExpiredRewardRound({hook: hook, token: token, round: rounds[i]});
321
-
322
- unchecked {
323
- // Safe because the loop is bounded by calldata length.
324
- ++i;
325
- }
326
- }
314
+ amount = _burnExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
327
315
  }
328
316
 
329
317
  /// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
@@ -347,21 +335,7 @@ abstract contract JBDistributor is IJBDistributor {
347
335
  external
348
336
  override
349
337
  {
350
- // Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
351
- _requireNotAcceptingToken();
352
-
353
- // Make sure that all staker token IDs are burned.
354
- for (uint256 i; i < tokenIds.length;) {
355
- if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
356
- revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
357
- }
358
- unchecked {
359
- ++i;
360
- }
361
- }
362
-
363
- // Unlock the rewards and recycle the forfeited amount.
364
- _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: false});
338
+ _releaseForfeitedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
365
339
  }
366
340
 
367
341
  //*********************************************************************//
@@ -391,7 +365,7 @@ abstract contract JBDistributor is IJBDistributor {
391
365
  override
392
366
  returns (uint256 tokenAmount)
393
367
  {
394
- tokenAmount = _unclaimedVestingAmountOf({hook: hook, tokenId: tokenId, token: token});
368
+ tokenAmount = _unclaimedVestingAmountOf({hook: hook, groupId: 0, tokenId: tokenId, token: token});
395
369
  }
396
370
 
397
371
  /// @notice Calculate how much of a reward token is currently unlocked and ready to be collected for a given
@@ -410,46 +384,7 @@ abstract contract JBDistributor is IJBDistributor {
410
384
  override
411
385
  returns (uint256 tokenAmount)
412
386
  {
413
- // A loan keeps this token ID's vesting rewards in collateral custody until the loan is repaid.
414
- if (activeVestingLoanIdOf[hook][tokenId][token] != 0) return 0;
415
-
416
- // The round that we are in right now.
417
- uint256 round = currentRound();
418
-
419
- // Keep a reference to the latest vested index.
420
- uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
421
-
422
- // Keep a reference to the vesting data array.
423
- JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
424
- uint256 numberOfVestingRounds = vestings.length;
425
-
426
- while (vestedIndex < numberOfVestingRounds) {
427
- uint256 lockedShare;
428
-
429
- // Keep a reference to the vested data being iterated on.
430
- JBVestingData memory vesting = vestings[vestedIndex];
431
-
432
- lockedShare = JBVestingMath.lockedShareOf({
433
- releaseRound: vesting.releaseRound,
434
- currentRound: round,
435
- vestingRounds: VESTING_ROUNDS,
436
- maxShare: MAX_SHARE
437
- });
438
-
439
- // Calculate the newly unlocked amount from cumulative shares rather than the incremental share delta.
440
- // Incremental floor rounding can otherwise underpay partial collections and leave dust stranded.
441
- (uint256 claimAmount,) = JBVestingMath.newlyClaimableAmountOf({
442
- amount: vesting.amount,
443
- shareClaimed: vesting.shareClaimed,
444
- lockedShare: lockedShare,
445
- maxShare: MAX_SHARE
446
- });
447
- tokenAmount += claimAmount;
448
-
449
- unchecked {
450
- ++vestedIndex;
451
- }
452
- }
387
+ tokenAmount = _collectableFor({hook: hook, groupId: 0, tokenId: tokenId, token: token});
453
388
  }
454
389
 
455
390
  /// @notice The vesting position collateralized by a Revnet loan.
@@ -495,21 +430,7 @@ abstract contract JBDistributor is IJBDistributor {
495
430
  virtual
496
431
  override
497
432
  {
498
- // Collections transfer reward tokens out; block them mid inbound transfer so the outgoing transfer cannot
499
- // net against the incoming balance delta and strand the new funds unaccounted.
500
- _requireNotAcceptingToken();
501
-
502
- // Revert if no token IDs are provided.
503
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
504
-
505
- // Only the entity authorized for these token IDs may materialize and collect their rewards.
506
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
507
-
508
- // Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
509
- _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
510
-
511
- // Release whatever portion of existing vesting entries has unlocked by this round.
512
- _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true});
433
+ _collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
513
434
  }
514
435
 
515
436
  /// @notice Borrow from a revnet using one token ID's uncollected vesting rewards as collateral.
@@ -538,41 +459,16 @@ abstract contract JBDistributor is IJBDistributor {
538
459
  override
539
460
  returns (uint256 loanId, uint256 collateralCount)
540
461
  {
541
- // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
542
- _requireNotAcceptingToken();
543
-
544
- // Revert if no token IDs are provided.
545
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
546
-
547
- // One distributor-held Revnet loan tracks one token ID so one repayment restores one vesting schedule.
548
- if (tokenIds.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokenIds.length});
549
-
550
- // One loan collateralizes one revnet reward token.
551
- if (tokens.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokens.length});
552
-
553
- // Zero vesting means rewards are immediately collectible, so there is no locked position to borrow against.
554
- if (VESTING_ROUNDS == 0) revert JBDistributor_VestingLoansDisabled();
555
-
556
- // Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
557
- if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
558
-
559
- // Make sure that all tokens can be claimed by this sender.
560
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
561
-
562
- // Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
563
- JBBorrowContext memory ctx = JBBorrowContext({
462
+ (loanId, collateralCount) = _borrowAgainstVestingFor({
564
463
  hook: hook,
565
- tokenId: tokenIds[0],
566
- token: tokens[0],
464
+ groupId: 0,
465
+ tokenIds: tokenIds,
466
+ tokens: tokens,
567
467
  sourceToken: sourceToken,
568
468
  minBorrowAmount: minBorrowAmount,
569
469
  prepaidFeePercent: prepaidFeePercent,
570
- beneficiary: beneficiary,
571
- revnetId: _revnetIdOf(tokens[0])
470
+ beneficiary: beneficiary
572
471
  });
573
-
574
- // Open and track the distributor-owned loan.
575
- (loanId, collateralCount) = _borrowAgainstVesting({ctx: ctx, tokenIds: tokenIds, tokens: tokens});
576
472
  }
577
473
 
578
474
  /// @notice Repay a distributor-held Revnet loan and restore its collateral to the original vesting schedule.
@@ -655,15 +551,210 @@ abstract contract JBDistributor is IJBDistributor {
655
551
 
656
552
  /// @notice Claim all past reward rounds for the given token IDs and reward tokens into fresh vesting entries.
657
553
  /// @param hook The hook whose stakers are claiming.
554
+ /// @param groupId The reward group being claimed (0 = the default group).
658
555
  /// @param tokenIds The token IDs to claim for.
659
556
  /// @param tokens The reward tokens to claim.
660
- function _claimPastRewards(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) internal virtual;
557
+ function _claimPastRewards(
558
+ address hook,
559
+ uint256 groupId,
560
+ uint256[] calldata tokenIds,
561
+ IERC20[] calldata tokens
562
+ )
563
+ internal
564
+ virtual;
661
565
 
662
566
  /// @notice Revert unless the caller is authorized to claim each token ID.
663
567
  /// @param hook The hook whose token IDs are being checked.
664
568
  /// @param tokenIds The token IDs to check.
665
569
  function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
666
570
 
571
+ /// @notice Shared begin-vesting logic across reward groups.
572
+ /// @param hook The hook whose stakers are vesting.
573
+ /// @param groupId The reward group (0 = the default group).
574
+ /// @param tokenIds The staker token IDs to claim rewards for.
575
+ /// @param tokens The reward tokens to begin vesting.
576
+ function _beginVesting(
577
+ address hook,
578
+ uint256 groupId,
579
+ uint256[] calldata tokenIds,
580
+ IERC20[] calldata tokens
581
+ )
582
+ internal
583
+ {
584
+ // Reward accounting cannot change while an ERC-20 `transferFrom` is in progress.
585
+ _requireNotAcceptingToken();
586
+
587
+ // Revert if no token IDs are provided.
588
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
589
+
590
+ // Only the entity authorized for these token IDs may start their vesting clock.
591
+ _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
592
+
593
+ // Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
594
+ _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
595
+ }
596
+
597
+ /// @notice Shared begin-vesting-then-collect logic across reward groups.
598
+ /// @param hook The hook whose stakers are collecting.
599
+ /// @param groupId The reward group (0 = the default group).
600
+ /// @param tokenIds The token IDs to collect for.
601
+ /// @param tokens The reward tokens to collect.
602
+ /// @param beneficiary The recipient of the collected tokens.
603
+ function _collectVestedRewards(
604
+ address hook,
605
+ uint256 groupId,
606
+ uint256[] calldata tokenIds,
607
+ IERC20[] calldata tokens,
608
+ address beneficiary
609
+ )
610
+ internal
611
+ {
612
+ // Collections transfer reward tokens out; block them mid inbound transfer.
613
+ _requireNotAcceptingToken();
614
+
615
+ // Revert if no token IDs are provided.
616
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
617
+
618
+ // Only the entity authorized for these token IDs may materialize and collect their rewards.
619
+ _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
620
+
621
+ // Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
622
+ _claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
623
+
624
+ // Release whatever portion of existing vesting entries has unlocked by this round.
625
+ _unlockRewards({
626
+ hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true
627
+ });
628
+ }
629
+
630
+ /// @notice Shared forfeiture-release logic across reward groups.
631
+ /// @param hook The hook whose tokens were burned.
632
+ /// @param groupId The reward group (0 = the default group).
633
+ /// @param tokenIds The IDs of the burned tokens.
634
+ /// @param tokens The reward tokens to recycle.
635
+ /// @param beneficiary Unused for forfeiture. Kept for interface compatibility.
636
+ function _releaseForfeitedRewards(
637
+ address hook,
638
+ uint256 groupId,
639
+ uint256[] calldata tokenIds,
640
+ IERC20[] calldata tokens,
641
+ address beneficiary
642
+ )
643
+ internal
644
+ {
645
+ // Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
646
+ _requireNotAcceptingToken();
647
+
648
+ // Make sure that all staker token IDs are burned.
649
+ for (uint256 i; i < tokenIds.length;) {
650
+ if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
651
+ revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
652
+ }
653
+ unchecked {
654
+ ++i;
655
+ }
656
+ }
657
+
658
+ // Unlock the rewards and recycle the forfeited amount.
659
+ _unlockRewards({
660
+ hook: hook,
661
+ groupId: groupId,
662
+ tokenIds: tokenIds,
663
+ tokens: tokens,
664
+ beneficiary: beneficiary,
665
+ ownerClaim: false
666
+ });
667
+ }
668
+
669
+ /// @notice Shared expired-reward recycling logic across reward groups.
670
+ /// @param hook The hook whose expired rewards should be recycled.
671
+ /// @param groupId The reward group (0 = the default group).
672
+ /// @param token The reward token to recycle.
673
+ /// @param rounds The reward rounds to recycle.
674
+ /// @return amount The total amount recycled.
675
+ function _burnExpiredRewards(
676
+ address hook,
677
+ uint256 groupId,
678
+ IERC20 token,
679
+ uint256[] calldata rounds
680
+ )
681
+ internal
682
+ returns (uint256 amount)
683
+ {
684
+ // Do not let reward-token callbacks recycle inventory during an inbound balance-delta measurement.
685
+ _requireNotAcceptingToken();
686
+
687
+ // Process every requested round independently so callers can batch keeper work.
688
+ for (uint256 i; i < rounds.length;) {
689
+ amount += _recycleExpiredRewardRound({hook: hook, groupId: groupId, token: token, round: rounds[i]});
690
+ unchecked {
691
+ ++i;
692
+ }
693
+ }
694
+ }
695
+
696
+ /// @notice Shared borrow-against-vesting logic across reward groups.
697
+ /// @param hook The hook whose staker is borrowing against vesting rewards.
698
+ /// @param groupId The reward group (0 = the default group).
699
+ /// @param tokenIds The single token ID to borrow against.
700
+ /// @param tokens The single revnet reward token to collateralize.
701
+ /// @param sourceToken The token to borrow from the revnet.
702
+ /// @param minBorrowAmount The minimum amount to borrow, denominated in `sourceToken`.
703
+ /// @param prepaidFeePercent The fee percent to charge upfront.
704
+ /// @param beneficiary The recipient of the borrowed funds.
705
+ /// @return loanId The Revnet loan NFT ID held by this distributor.
706
+ /// @return collateralCount The amount of vesting rewards used as collateral.
707
+ function _borrowAgainstVestingFor(
708
+ address hook,
709
+ uint256 groupId,
710
+ uint256[] calldata tokenIds,
711
+ IERC20[] calldata tokens,
712
+ address sourceToken,
713
+ uint256 minBorrowAmount,
714
+ uint256 prepaidFeePercent,
715
+ address payable beneficiary
716
+ )
717
+ internal
718
+ returns (uint256 loanId, uint256 collateralCount)
719
+ {
720
+ // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
721
+ _requireNotAcceptingToken();
722
+
723
+ // Revert if no token IDs are provided.
724
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
725
+
726
+ // One distributor-held Revnet loan tracks one token ID so one repayment restores one vesting schedule.
727
+ if (tokenIds.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokenIds.length});
728
+
729
+ // One loan collateralizes one revnet reward token.
730
+ if (tokens.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokens.length});
731
+
732
+ // Zero vesting means rewards are immediately collectible, so there is no locked position to borrow against.
733
+ if (VESTING_ROUNDS == 0) revert JBDistributor_VestingLoansDisabled();
734
+
735
+ // Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
736
+ if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
737
+
738
+ // Make sure that all tokens can be claimed by this sender.
739
+ _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
740
+
741
+ // Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
742
+ JBBorrowContext memory ctx = JBBorrowContext({
743
+ hook: hook,
744
+ groupId: groupId,
745
+ tokenId: tokenIds[0],
746
+ token: tokens[0],
747
+ sourceToken: sourceToken,
748
+ minBorrowAmount: minBorrowAmount,
749
+ prepaidFeePercent: prepaidFeePercent,
750
+ beneficiary: beneficiary,
751
+ revnetId: _revnetIdOf(tokens[0])
752
+ });
753
+
754
+ // Open and track the distributor-owned loan.
755
+ (loanId, collateralCount) = _borrowAgainstVesting({ctx: ctx, tokenIds: tokenIds, tokens: tokens});
756
+ }
757
+
667
758
  /// @notice Open and track a distributor-held Revnet loan against one vesting position.
668
759
  /// @param ctx The borrow context.
669
760
  /// @param tokenIds The single token ID being collateralized.
@@ -679,7 +770,7 @@ abstract contract JBDistributor is IJBDistributor {
679
770
  returns (uint256 loanId, uint256 collateralCount)
680
771
  {
681
772
  // One vesting position cannot be collateralized by two outstanding loans.
682
- uint256 activeLoanId = activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token];
773
+ uint256 activeLoanId = activeVestingLoanIdOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token];
683
774
  if (activeLoanId != 0) {
684
775
  revert JBDistributor_VestingLoanOutstanding({
685
776
  hook: ctx.hook, tokenId: ctx.tokenId, token: address(ctx.token), loanId: activeLoanId
@@ -687,13 +778,14 @@ abstract contract JBDistributor is IJBDistributor {
687
778
  }
688
779
 
689
780
  // Bring the claimant current before measuring collateral.
690
- _claimPastRewards({hook: ctx.hook, tokenIds: tokenIds, tokens: tokens});
781
+ _claimPastRewards({hook: ctx.hook, groupId: ctx.groupId, tokenIds: tokenIds, tokens: tokens});
691
782
 
692
783
  // Use the remaining uncollected vesting amount as collateral without advancing the vesting schedule.
693
- collateralCount = _unclaimedVestingAmountOf({hook: ctx.hook, tokenId: ctx.tokenId, token: ctx.token});
784
+ collateralCount =
785
+ _unclaimedVestingAmountOf({hook: ctx.hook, groupId: ctx.groupId, tokenId: ctx.tokenId, token: ctx.token});
694
786
 
695
787
  // Remember the vesting-entry boundary so liquidation write-off cannot consume later rewards.
696
- uint48 vestingDataCount = _toUint48(vestingDataOf[ctx.hook][ctx.tokenId][ctx.token].length);
788
+ uint48 vestingDataCount = _toUint48(vestingDataOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token].length);
697
789
 
698
790
  // A zero-collateral loan would revert in Revnet, but this local error explains why.
699
791
  if (collateralCount == 0) {
@@ -706,7 +798,7 @@ abstract contract JBDistributor is IJBDistributor {
706
798
  totalLoanedVestingAmountOf[ctx.hook][ctx.token] += collateralCount;
707
799
 
708
800
  // Block same-position reentrancy before the loan contract burns collateral and returns the real loan ID.
709
- activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token] = _PENDING_VESTING_LOAN_ID;
801
+ activeVestingLoanIdOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token] = _PENDING_VESTING_LOAN_ID;
710
802
 
711
803
  // Open the Revnet loan with this distributor as the holder whose tokens are burned as collateral.
712
804
  loanId = _openVestingLoan({ctx: ctx, collateralCount: collateralCount});
@@ -715,9 +807,10 @@ abstract contract JBDistributor is IJBDistributor {
715
807
  }
716
808
 
717
809
  // Track the distributor-held loan so repayment can restore the same vesting position.
718
- activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token] = loanId;
810
+ activeVestingLoanIdOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token] = loanId;
719
811
  _vestingLoanOf[loanId] = JBVestingLoan({
720
812
  hook: ctx.hook,
813
+ groupId: ctx.groupId,
721
814
  tokenId: ctx.tokenId,
722
815
  token: ctx.token,
723
816
  vestingDataCount: vestingDataCount,
@@ -867,7 +960,7 @@ abstract contract JBDistributor is IJBDistributor {
867
960
  totalLoanedVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= vestingLoan.collateralCount;
868
961
 
869
962
  // Clear the lock that prevented this position from being collected while collateralized.
870
- delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
963
+ delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
871
964
  delete _vestingLoanOf[loanId];
872
965
 
873
966
  // Return any excess reward tokens created during source-fee payment to the repayer.
@@ -901,10 +994,12 @@ abstract contract JBDistributor is IJBDistributor {
901
994
  collateralCount = vestingLoan.collateralCount;
902
995
 
903
996
  // Load the vesting entries for the token ID whose rewards were collateralized.
904
- JBVestingData[] storage vestings = vestingDataOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
997
+ JBVestingData[] storage vestings =
998
+ vestingDataOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
905
999
 
906
1000
  // Start at the first unexhausted vesting entry.
907
- uint256 vestedIndex = latestVestedIndexOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
1001
+ uint256 vestedIndex =
1002
+ latestVestedIndexOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
908
1003
 
909
1004
  // Stop at the boundary recorded when the loan opened, preserving newer vesting entries.
910
1005
  uint256 vestingDataCount = vestingLoan.vestingDataCount;
@@ -920,7 +1015,7 @@ abstract contract JBDistributor is IJBDistributor {
920
1015
  }
921
1016
 
922
1017
  // Skip over the written-off vesting entries without ever moving the cursor backwards.
923
- latestVestedIndexOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token] = vestedIndex;
1018
+ latestVestedIndexOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token] = vestedIndex;
924
1019
 
925
1020
  // Remove the liquidated collateral from the amount still considered vesting.
926
1021
  totalVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= collateralCount;
@@ -929,7 +1024,7 @@ abstract contract JBDistributor is IJBDistributor {
929
1024
  totalLoanedVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= collateralCount;
930
1025
 
931
1026
  // Clear the active loan lock for this token ID and reward token.
932
- delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
1027
+ delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
933
1028
 
934
1029
  // Clear the loan metadata so it cannot be written off or repaid again.
935
1030
  delete _vestingLoanOf[loanId];
@@ -980,9 +1075,10 @@ abstract contract JBDistributor is IJBDistributor {
980
1075
 
981
1076
  /// @notice Accept funds and assign them to this round's reward ledger.
982
1077
  /// @param hook The stake source whose stakers receive the rewards.
1078
+ /// @param groupId The reward group being funded (0 = the default group).
983
1079
  /// @param token The reward token being funded.
984
1080
  /// @param amount The nominal amount to fund.
985
- function _fund(address hook, IERC20 token, uint256 amount) internal {
1081
+ function _fund(address hook, uint256 groupId, IERC20 token, uint256 amount) internal {
986
1082
  // Native funding is measured by msg.value, not the caller-provided amount.
987
1083
  if (address(token) == JBConstants.NATIVE_TOKEN) {
988
1084
  amount = msg.value;
@@ -997,38 +1093,41 @@ abstract contract JBDistributor is IJBDistributor {
997
1093
  }
998
1094
 
999
1095
  // Store the accepted amount in this round's historical reward ledger.
1000
- _recordRewardFunding({hook: hook, token: token, amount: amount});
1096
+ _recordRewardFunding({hook: hook, groupId: groupId, token: token, amount: amount});
1001
1097
  }
1002
1098
 
1003
1099
  /// @notice Record accepted funding as the current round's reward pot.
1004
1100
  /// @param hook The stake source whose stakers receive the rewards.
1101
+ /// @param groupId The reward group (0 = the default group).
1005
1102
  /// @param token The reward token.
1006
1103
  /// @param amount The accepted funding amount.
1007
- function _recordRewardFunding(address hook, IERC20 token, uint256 amount) internal {
1104
+ function _recordRewardFunding(address hook, uint256 groupId, IERC20 token, uint256 amount) internal {
1008
1105
  // Zero-value transfers do not create reward rounds or alter tracked balances.
1009
1106
  if (amount == 0) return;
1010
1107
 
1011
1108
  // Add the accepted amount to the current reward ledger.
1012
- _recordRewardRound({hook: hook, token: token, amount: amount});
1109
+ _recordRewardRound({hook: hook, groupId: groupId, token: token, amount: amount});
1013
1110
 
1014
- // Keep the base distributor's balance accounting in sync for collection and conservation checks.
1111
+ // Keep the base distributor's balance accounting in sync for collection and conservation checks. Balances
1112
+ // are tracked per (hook, token) across all groups because they share one token custody pool.
1015
1113
  _balanceOf[hook][token] += amount;
1016
1114
  _accountedBalanceOf[token] += amount;
1017
1115
  }
1018
1116
 
1019
1117
  /// @notice Record rewards as the current round's claimable historical reward pot.
1020
1118
  /// @param hook The stake source whose stakers receive the rewards.
1119
+ /// @param groupId The reward group (0 = the default group).
1021
1120
  /// @param token The reward token.
1022
1121
  /// @param amount The amount to add to the current reward round.
1023
- function _recordRewardRound(address hook, IERC20 token, uint256 amount) internal {
1122
+ function _recordRewardRound(address hook, uint256 groupId, IERC20 token, uint256 amount) internal {
1024
1123
  // Zero-value rewards do not create reward rounds.
1025
1124
  if (amount == 0) return;
1026
1125
 
1027
1126
  // Rewards belong to the round in progress when they enter the ledger.
1028
1127
  uint256 round = currentRound();
1029
1128
 
1030
- // Load the current round's ledger entry for this hook and reward token.
1031
- JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
1129
+ // Load the current round's ledger entry for this hook, group, and reward token.
1130
+ JBRewardRoundData storage rewardRound = rewardRoundOf[hook][groupId][token][round];
1032
1131
 
1033
1132
  // Every reward round in this contract uses the same immutable claim duration.
1034
1133
  uint48 claimDeadline = _claimDeadlineFor(round);
@@ -1044,8 +1143,8 @@ abstract contract JBDistributor is IJBDistributor {
1044
1143
  // Store the packed claim deadline fixed for this distributor.
1045
1144
  rewardRound.claimDeadline = claimDeadline;
1046
1145
 
1047
- // Store the packed total stake that shares this round's reward pot.
1048
- rewardRound.totalStake = _toUint208(_totalStake({hook: hook, blockNumber: snapshotBlock}));
1146
+ // Store the packed total stake that shares this group's round reward pot.
1147
+ rewardRound.totalStake = _toUint208(_totalStake({hook: hook, groupId: groupId, blockNumber: snapshotBlock}));
1049
1148
  }
1050
1149
 
1051
1150
  // Multiple additions in the same round share the same snapshot and reward pot.
@@ -1054,11 +1153,13 @@ abstract contract JBDistributor is IJBDistributor {
1054
1153
 
1055
1154
  /// @notice Recycle one expired reward round's unclaimed inventory into the current reward round.
1056
1155
  /// @param hook The hook whose expired rewards should be recycled.
1156
+ /// @param groupId The reward group (0 = the default group).
1057
1157
  /// @param token The reward token to recycle.
1058
1158
  /// @param round The reward round to recycle.
1059
1159
  /// @return recycleAmount The amount recycled.
1060
1160
  function _recycleExpiredRewardRound(
1061
1161
  address hook,
1162
+ uint256 groupId,
1062
1163
  IERC20 token,
1063
1164
  uint256 round
1064
1165
  )
@@ -1066,7 +1167,7 @@ abstract contract JBDistributor is IJBDistributor {
1066
1167
  returns (uint256 recycleAmount)
1067
1168
  {
1068
1169
  // Load the reward round once so expiry, claimed amount, and funded amount stay in sync.
1069
- JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
1170
+ JBRewardRoundData storage rewardRound = rewardRoundOf[hook][groupId][token][round];
1070
1171
 
1071
1172
  // Ignore rounds that either never expire or have not reached their deadline yet.
1072
1173
  if (!_rewardRoundExpired(rewardRound)) return 0;
@@ -1082,7 +1183,7 @@ abstract contract JBDistributor is IJBDistributor {
1082
1183
 
1083
1184
  // Keep the inventory in the distributor and give the current staker set a new claimable round.
1084
1185
  uint256 recycledToRound = currentRound();
1085
- _recordRewardRound({hook: hook, token: token, amount: recycleAmount});
1186
+ _recordRewardRound({hook: hook, groupId: groupId, token: token, amount: recycleAmount});
1086
1187
 
1087
1188
  // Surface the permissionless recycle for off-chain accounting.
1088
1189
  emit ExpiredRewardsRecycled({
@@ -1174,12 +1275,14 @@ abstract contract JBDistributor is IJBDistributor {
1174
1275
 
1175
1276
  /// @notice Unlocks rewards for the given token IDs and tokens, either for collection or forfeiture.
1176
1277
  /// @param hook The hook the tokens belong to.
1278
+ /// @param groupId The reward group (0 = the default group).
1177
1279
  /// @param tokenIds The IDs of the tokens to unlock rewards for.
1178
1280
  /// @param tokens The addresses of the tokens to unlock.
1179
1281
  /// @param beneficiary The recipient of the unlocked tokens.
1180
1282
  /// @param ownerClaim Whether this is a claim by the owner (true) or a forfeiture release (false).
1181
1283
  function _unlockRewards(
1182
1284
  address hook,
1285
+ uint256 groupId,
1183
1286
  uint256[] calldata tokenIds,
1184
1287
  IERC20[] calldata tokens,
1185
1288
  address beneficiary,
@@ -1194,7 +1297,8 @@ abstract contract JBDistributor is IJBDistributor {
1194
1297
  IERC20 token = tokens[i];
1195
1298
 
1196
1299
  // Process all token IDs for this reward token.
1197
- uint256 totalTokenAmount = _unlockTokenIds({hook: hook, tokenIds: tokenIds, token: token, round: round});
1300
+ uint256 totalTokenAmount =
1301
+ _unlockTokenIds({hook: hook, groupId: groupId, tokenIds: tokenIds, token: token, round: round});
1198
1302
 
1199
1303
  // Perform the transfer.
1200
1304
  if (totalTokenAmount != 0) {
@@ -1221,7 +1325,7 @@ abstract contract JBDistributor is IJBDistributor {
1221
1325
  }
1222
1326
  } else {
1223
1327
  // If forfeiture: keep inventory in the distributor and give the current staker set a fresh round.
1224
- _recordRewardRound({hook: hook, token: token, amount: totalTokenAmount});
1328
+ _recordRewardRound({hook: hook, groupId: groupId, token: token, amount: totalTokenAmount});
1225
1329
  emit ForfeitedRewardsRecycled({
1226
1330
  hook: hook, round: round, token: token, amount: totalTokenAmount, caller: msg.sender
1227
1331
  });
@@ -1236,12 +1340,14 @@ abstract contract JBDistributor is IJBDistributor {
1236
1340
 
1237
1341
  /// @notice Unlocks rewards for a set of token IDs for a single reward token.
1238
1342
  /// @param hook The hook the tokens belong to.
1343
+ /// @param groupId The reward group (0 = the default group).
1239
1344
  /// @param tokenIds The IDs of the tokens to unlock rewards for.
1240
1345
  /// @param token The reward token to unlock.
1241
1346
  /// @param round The current round.
1242
1347
  /// @return totalTokenAmount The total amount of reward tokens unlocked.
1243
1348
  function _unlockTokenIds(
1244
1349
  address hook,
1350
+ uint256 groupId,
1245
1351
  uint256[] calldata tokenIds,
1246
1352
  IERC20 token,
1247
1353
  uint256 round
@@ -1253,13 +1359,13 @@ abstract contract JBDistributor is IJBDistributor {
1253
1359
  uint256 tokenId = tokenIds[j];
1254
1360
 
1255
1361
  // Loan collateral stays locked until repayment restores it to this distributor.
1256
- _requireNoActiveVestingLoan({hook: hook, tokenId: tokenId, token: token});
1362
+ _requireNoActiveVestingLoan({hook: hook, groupId: groupId, tokenId: tokenId, token: token});
1257
1363
 
1258
1364
  // Keep a reference to the latest vested index.
1259
- uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
1365
+ uint256 vestedIndex = latestVestedIndexOf[hook][groupId][tokenId][token];
1260
1366
 
1261
1367
  // Keep a reference to the vesting data array.
1262
- JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
1368
+ JBVestingData[] storage vestings = vestingDataOf[hook][groupId][tokenId][token];
1263
1369
  uint256 numberOfVestingRounds = vestings.length;
1264
1370
 
1265
1371
  // Keep a reference to a vested index that will be incremented.
@@ -1294,6 +1400,7 @@ abstract contract JBDistributor is IJBDistributor {
1294
1400
  emit Collected({
1295
1401
  hook: hook,
1296
1402
  tokenId: tokenId,
1403
+ groupId: groupId,
1297
1404
  token: token,
1298
1405
  amount: claimAmount,
1299
1406
  vestingReleaseRound: vesting.releaseRound,
@@ -1315,7 +1422,7 @@ abstract contract JBDistributor is IJBDistributor {
1315
1422
  }
1316
1423
  }
1317
1424
 
1318
- latestVestedIndexOf[hook][tokenId][token] = newLatestVestedIndex;
1425
+ latestVestedIndexOf[hook][groupId][tokenId][token] = newLatestVestedIndex;
1319
1426
 
1320
1427
  unchecked {
1321
1428
  ++j;
@@ -1327,13 +1434,73 @@ abstract contract JBDistributor is IJBDistributor {
1327
1434
  // ----------------------- internal views ---------------------------- //
1328
1435
  //*********************************************************************//
1329
1436
 
1437
+ /// @notice The collectable (unlocked, uncollected) amount for a token ID in a specific reward group.
1438
+ /// @param hook The hook the tokenId belongs to.
1439
+ /// @param groupId The reward group (0 = the default group).
1440
+ /// @param tokenId The ID of the staker token to calculate for.
1441
+ /// @param token The reward token to check.
1442
+ /// @return tokenAmount The amount of tokens that can be collected right now.
1443
+ function _collectableFor(
1444
+ address hook,
1445
+ uint256 groupId,
1446
+ uint256 tokenId,
1447
+ IERC20 token
1448
+ )
1449
+ internal
1450
+ view
1451
+ returns (uint256 tokenAmount)
1452
+ {
1453
+ // A loan keeps this token ID's vesting rewards in collateral custody until the loan is repaid.
1454
+ if (activeVestingLoanIdOf[hook][groupId][tokenId][token] != 0) return 0;
1455
+
1456
+ // The round that we are in right now.
1457
+ uint256 round = currentRound();
1458
+
1459
+ // Keep a reference to the latest vested index.
1460
+ uint256 vestedIndex = latestVestedIndexOf[hook][groupId][tokenId][token];
1461
+
1462
+ // Keep a reference to the vesting data array.
1463
+ JBVestingData[] storage vestings = vestingDataOf[hook][groupId][tokenId][token];
1464
+ uint256 numberOfVestingRounds = vestings.length;
1465
+
1466
+ while (vestedIndex < numberOfVestingRounds) {
1467
+ uint256 lockedShare;
1468
+
1469
+ // Keep a reference to the vested data being iterated on.
1470
+ JBVestingData memory vesting = vestings[vestedIndex];
1471
+
1472
+ lockedShare = JBVestingMath.lockedShareOf({
1473
+ releaseRound: vesting.releaseRound,
1474
+ currentRound: round,
1475
+ vestingRounds: VESTING_ROUNDS,
1476
+ maxShare: MAX_SHARE
1477
+ });
1478
+
1479
+ // Calculate the newly unlocked amount from cumulative shares rather than the incremental share delta.
1480
+ // Incremental floor rounding can otherwise underpay partial collections and leave dust stranded.
1481
+ (uint256 claimAmount,) = JBVestingMath.newlyClaimableAmountOf({
1482
+ amount: vesting.amount,
1483
+ shareClaimed: vesting.shareClaimed,
1484
+ lockedShare: lockedShare,
1485
+ maxShare: MAX_SHARE
1486
+ });
1487
+ tokenAmount += claimAmount;
1488
+
1489
+ unchecked {
1490
+ ++vestedIndex;
1491
+ }
1492
+ }
1493
+ }
1494
+
1330
1495
  /// @notice The remaining uncollected vesting amount for one token ID and reward token.
1331
1496
  /// @param hook The hook the token ID belongs to.
1497
+ /// @param groupId The reward group (0 = the default group).
1332
1498
  /// @param tokenId The token ID to check.
1333
1499
  /// @param token The reward token to check.
1334
1500
  /// @return tokenAmount The amount still locked or unlocked-but-uncollected.
1335
1501
  function _unclaimedVestingAmountOf(
1336
1502
  address hook,
1503
+ uint256 groupId,
1337
1504
  uint256 tokenId,
1338
1505
  IERC20 token
1339
1506
  )
@@ -1342,10 +1509,10 @@ abstract contract JBDistributor is IJBDistributor {
1342
1509
  returns (uint256 tokenAmount)
1343
1510
  {
1344
1511
  // Keep a reference to the latest fully vested index.
1345
- uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
1512
+ uint256 vestedIndex = latestVestedIndexOf[hook][groupId][tokenId][token];
1346
1513
 
1347
1514
  // Keep a reference to the vesting data array.
1348
- JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
1515
+ JBVestingData[] storage vestings = vestingDataOf[hook][groupId][tokenId][token];
1349
1516
  uint256 numberOfVestingRounds = vestings.length;
1350
1517
 
1351
1518
  while (vestedIndex < numberOfVestingRounds) {
@@ -1381,10 +1548,11 @@ abstract contract JBDistributor is IJBDistributor {
1381
1548
 
1382
1549
  /// @notice Revert if a token ID's vesting rewards are locked in a distributor-owned loan.
1383
1550
  /// @param hook The hook the token ID belongs to.
1551
+ /// @param groupId The reward group (0 = the default group).
1384
1552
  /// @param tokenId The token ID to check.
1385
1553
  /// @param token The reward token to check.
1386
- function _requireNoActiveVestingLoan(address hook, uint256 tokenId, IERC20 token) internal view {
1387
- uint256 loanId = activeVestingLoanIdOf[hook][tokenId][token];
1554
+ function _requireNoActiveVestingLoan(address hook, uint256 groupId, uint256 tokenId, IERC20 token) internal view {
1555
+ uint256 loanId = activeVestingLoanIdOf[hook][groupId][tokenId][token];
1388
1556
  if (loanId != 0) {
1389
1557
  revert JBDistributor_VestingLoanOutstanding({
1390
1558
  hook: hook, tokenId: tokenId, token: address(token), loanId: loanId
@@ -1400,17 +1568,26 @@ abstract contract JBDistributor is IJBDistributor {
1400
1568
  function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
1401
1569
 
1402
1570
  /// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
1403
- /// For 721 distributors this is the tier's voting units; for token distributors this is delegated voting power.
1571
+ /// @dev Subclasses define how stake is measured.
1404
1572
  /// @param hook The hook the token belongs to.
1405
1573
  /// @param tokenId The ID of the token to get the stake weight of.
1406
1574
  /// @return tokenStakeAmount The stake weight represented by this token ID.
1407
1575
  function _tokenStake(address hook, uint256 tokenId) internal view virtual returns (uint256 tokenStakeAmount);
1408
1576
 
1409
- /// @notice The total stake across all token IDs at a given block. Used as the denominator when calculating each
1410
- /// token ID's pro-rata share. For 721 distributors this is `getPastTotalSupply` from the checkpoints module;
1411
- /// for token distributors this is `getPastTotalSupply` from the IVotes token.
1577
+ /// @notice The total stake sharing a group's round rewards at a given block. Used as the denominator when
1578
+ /// calculating each token ID's pro-rata share.
1579
+ /// @dev Subclasses define how the per-group total stake is measured.
1412
1580
  /// @param hook The hook to get the total stake for.
1581
+ /// @param groupId The reward group (0 = the default group).
1413
1582
  /// @param blockNumber The block number to query (must be strictly in the past).
1414
1583
  /// @return totalStakedAmount The total stake at the given block.
1415
- function _totalStake(address hook, uint256 blockNumber) internal view virtual returns (uint256 totalStakedAmount);
1584
+ function _totalStake(
1585
+ address hook,
1586
+ uint256 groupId,
1587
+ uint256 blockNumber
1588
+ )
1589
+ internal
1590
+ view
1591
+ virtual
1592
+ returns (uint256 totalStakedAmount);
1416
1593
  }