@bananapus/distributor-v6 0.0.33 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/distributor-v6",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -162,57 +162,10 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
162
162
  }
163
163
  }
164
164
 
165
- /// @notice Snapshot this NFT's past reward rounds and start vesting them now.
166
- /// @dev Current-round funding is excluded. It becomes claimable once a later round starts.
167
- /// @param hook The 721 hook whose NFTs are vesting.
168
- /// @param tokenIds The NFT token IDs to claim for.
169
- /// @param tokens The reward tokens to begin vesting.
170
- function beginVesting(
171
- address hook,
172
- uint256[] calldata tokenIds,
173
- IERC20[] calldata tokens
174
- )
175
- external
176
- override(JBDistributor, IJBDistributor)
177
- {
178
- // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
179
- _requireNotAcceptingToken();
180
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
181
-
182
- // Only the current NFT owner can start vesting for each token ID.
183
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
184
-
185
- // Materialize all unclaimed historical rewards into fresh vesting entries that start now.
186
- _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
187
- }
188
-
189
- /// @notice Collect already-vested rewards and first start vesting any unclaimed past reward rounds.
190
- /// @param hook The 721 hook whose NFTs are collecting.
191
- /// @param tokenIds The NFT token IDs to collect for.
192
- /// @param tokens The reward tokens to collect.
193
- /// @param beneficiary The recipient of collected vested rewards.
194
- function collectVestedRewards(
195
- address hook,
196
- uint256[] calldata tokenIds,
197
- IERC20[] calldata tokens,
198
- address beneficiary
199
- )
200
- public
201
- override(JBDistributor, IJBDistributor)
202
- {
203
- // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
204
- _requireNotAcceptingToken();
205
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
206
-
207
- // Only the current NFT owner can materialize and collect rewards for each token ID.
208
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
209
-
210
- // Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
211
- _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
212
-
213
- // Release whatever portion of existing vesting entries has unlocked by this round.
214
- _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true});
215
- }
165
+ // `beginVesting` and `collectVestedRewards` are provided by `JBDistributor`. Both distributors share the exact
166
+ // same flow (authorize -> materialize past rounds via `_claimPastRewards` -> optionally release unlocked), so the
167
+ // round-claim logic lives once in the base and dispatches to this contract's `_claimPastRewards` /
168
+ // `_requireCanClaimTokenIds` overrides below.
216
169
 
217
170
  //*********************************************************************//
218
171
  // -------------------------- public views --------------------------- //
@@ -473,75 +426,6 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
473
426
  consumed[ownerIndex] += stake;
474
427
  }
475
428
 
476
- /// @notice Override vesting to cap each owner's consumed voting power across all their NFTs.
477
- /// @dev Prevents an owner with N NFTs of V voting units each from claiming N*V when their pastVotes < N*V.
478
- /// Iterates over all token IDs in the batch, delegating per-token logic to `_vestSingleToken`. A pair of
479
- /// scratch arrays (`owners` and `consumed`) tracks how much voting power each distinct owner has used so far,
480
- /// ensuring the aggregate claim never exceeds the owner's snapshot voting power.
481
- /// Silently skips burned tokens, already-vested tokens, and tokens whose owner had no snapshot voting power.
482
- /// @param hook The address of the 721 hook whose stakers are vesting.
483
- /// @param tokenIds The NFT token IDs to vest rewards for.
484
- /// @param token The ERC-20 reward token to distribute.
485
- /// @param distributable The total distributable amount of `token` for this round.
486
- /// @param totalStakeAmount The aggregate voting power at the round's snapshot block.
487
- /// @param vestingReleaseRound The round number at which the vesting period ends and tokens become fully claimable.
488
- /// @return totalVestingAmount The sum of reward tokens that began vesting across all processed token IDs.
489
- function _vestTokenIds(
490
- address hook,
491
- uint256[] calldata tokenIds,
492
- IERC20 token,
493
- uint256 distributable,
494
- uint256 totalStakeAmount,
495
- uint256 vestingReleaseRound
496
- )
497
- internal
498
- override
499
- returns (uint256 totalVestingAmount)
500
- {
501
- // Bundle iteration-constant parameters into a struct to avoid stack-too-deep errors.
502
- JBVestContext memory ctx = JBVestContext({
503
- hook: hook,
504
- token: token,
505
- distributable: distributable,
506
- totalStakeAmount: totalStakeAmount,
507
- vestingReleaseRound: vestingReleaseRound,
508
- rewardRound: currentRound(),
509
- snapshotBlock: roundSnapshotBlock[currentRound()]
510
- });
511
-
512
- // Allocate scratch arrays sized to the maximum possible number of distinct owners (one per token ID).
513
- address[] memory owners = new address[](tokenIds.length);
514
- uint256[] memory consumed = new uint256[](tokenIds.length);
515
-
516
- // Track how many distinct owners have been recorded in the scratch arrays so far.
517
- uint256 uniqueCount;
518
-
519
- // Iterate over every token ID in the batch.
520
- for (uint256 j; j < tokenIds.length;) {
521
- // Vest the single token, receiving its reward amount and the updated distinct owner count.
522
- (uint256 tokenAmount, uint256 newUniqueCount) = _vestSingleToken({
523
- ctx: ctx, tokenId: tokenIds[j], owners: owners, consumed: consumed, uniqueCount: uniqueCount
524
- });
525
-
526
- // Carry the updated owner count forward so subsequent tokens can reference the same tracking data.
527
- uniqueCount = newUniqueCount;
528
-
529
- unchecked {
530
- // Accumulate the individual token's reward into the batch-wide total.
531
- totalVestingAmount += tokenAmount;
532
- ++j;
533
- }
534
- }
535
-
536
- // Persist consumed voting power to storage to prevent cap resets across calls.
537
- for (uint256 k; k < uniqueCount;) {
538
- _consumedVotesOf[hook][token][ctx.rewardRound][owners[k]] = consumed[k];
539
- unchecked {
540
- ++k;
541
- }
542
- }
543
- }
544
-
545
429
  //*********************************************************************//
546
430
  // ----------------------- internal views ---------------------------- //
547
431
  //*********************************************************************//
@@ -664,132 +548,4 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
664
548
  // A zero owner means the token was not owned at the snapshot block and is not eligible this round.
665
549
  owner = abi.decode(data, (address));
666
550
  }
667
-
668
- /// @notice Vest a single NFT token, enforcing a per-owner voting power cap across the batch.
669
- /// @dev Returns 0 for burned tokens, already-vested tokens, tokens whose owner had no snapshot voting power,
670
- /// and tokens whose owner has already exhausted their voting power cap within this batch.
671
- /// The `owners` and `consumed` arrays form a compact map that tracks how much voting power each unique
672
- /// owner has consumed so far. `uniqueCount` tracks how many slots are used.
673
- /// @param ctx The vesting context containing hook address, reward token, distributable amount, total stake,
674
- /// and release round.
675
- /// @param tokenId The NFT token ID to process.
676
- /// @param owners A scratch array mapping slot indices to owner addresses for deduplication within this batch.
677
- /// @param consumed A scratch array tracking how much voting power each owner (by slot index) has consumed.
678
- /// @param uniqueCount The number of distinct owners seen so far in the batch.
679
- /// @return tokenAmount The reward amount vested for this token ID (0 if skipped).
680
- /// @return newUniqueCount The updated count of distinct owners after processing this token ID.
681
- function _vestSingleToken(
682
- JBVestContext memory ctx,
683
- uint256 tokenId,
684
- address[] memory owners,
685
- uint256[] memory consumed,
686
- uint256 uniqueCount
687
- )
688
- private
689
- returns (uint256 tokenAmount, uint256 newUniqueCount)
690
- {
691
- // Initialize the return value to the current count of distinct owners.
692
- newUniqueCount = uniqueCount;
693
-
694
- // Skip burned tokens — they are excluded from _totalStake, so including them would overbook vesting.
695
- if (_tokenBurned({hook: ctx.hook, tokenId: tokenId})) return (0, newUniqueCount);
696
-
697
- // Skip already-vested tokenIds — check if the last vesting entry targets the same release round.
698
- {
699
- // Load the number of existing vesting entries for this token.
700
- JBVestingData[] storage vestings = vestingDataOf[ctx.hook][tokenId][ctx.token];
701
- uint256 numVesting = vestings.length;
702
-
703
- // If at least one entry exists and its release round matches, this token was already vested this round.
704
- if (numVesting != 0 && vestings[numVesting - 1].releaseRound == ctx.vestingReleaseRound) {
705
- return (0, newUniqueCount);
706
- }
707
- }
708
-
709
- // Look up the NFT's voting units from its tier in the hook's store.
710
- uint256 votingUnits =
711
- IJB721TiersHook(ctx.hook)
712
- .STORE()
713
- .tierOfTokenId({hook: ctx.hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
714
-
715
- // Look up the snapshot owner, verify snapshot eligibility, and find or create the owner's tracking slot.
716
- uint256 ownerIndex;
717
- uint256 pastVotes;
718
- {
719
- // Reuse the same round snapshot block for every token in this vesting batch.
720
- uint256 snapshotBlock = ctx.snapshotBlock;
721
- address owner = _snapshotOwnerOf({hook: ctx.hook, tokenId: tokenId, snapshotBlock: snapshotBlock});
722
- if (owner == address(0)) return (0, newUniqueCount);
723
-
724
- // Query the owner's checkpointed voting power at the round's snapshot block.
725
- pastVotes = IVotes(address(IJB721TiersHook(ctx.hook).checkpoints()))
726
- .getPastVotes({account: owner, timepoint: snapshotBlock});
727
-
728
- // If the snapshot owner had no voting power at the snapshot block, the token is ineligible for this round.
729
- if (pastVotes == 0) return (0, newUniqueCount);
730
-
731
- // Search the owners array for an existing slot belonging to this owner.
732
- bool found;
733
- for (uint256 k; k < newUniqueCount;) {
734
- if (owners[k] == owner) {
735
- // Re-use the existing tracking slot for this owner.
736
- ownerIndex = k;
737
- found = true;
738
- break;
739
- }
740
- unchecked {
741
- ++k;
742
- }
743
- }
744
-
745
- // If no existing slot was found, allocate a new one at the end of the arrays.
746
- if (!found) {
747
- ownerIndex = newUniqueCount;
748
- owners[newUniqueCount] = owner;
749
- // Initialize from persistent storage to prevent cap resets across calls.
750
- consumed[newUniqueCount] = _consumedVotesOf[ctx.hook][ctx.token][ctx.rewardRound][owner];
751
- unchecked {
752
- ++newUniqueCount;
753
- }
754
- }
755
- }
756
-
757
- // Cap this NFT's effective stake at the owner's remaining voting power budget for this batch.
758
- uint256 stake;
759
- {
760
- // Calculate how much voting power the owner has left after prior tokens in this batch.
761
- uint256 remaining = pastVotes > consumed[ownerIndex] ? pastVotes - consumed[ownerIndex] : 0;
762
-
763
- // The effective stake is the lesser of the NFT's voting units and the owner's remaining budget.
764
- stake = votingUnits < remaining ? votingUnits : remaining;
765
- }
766
-
767
- // If the effective stake is zero, the owner's budget is exhausted — skip this token.
768
- if (stake == 0) return (0, newUniqueCount);
769
-
770
- // Calculate the pro-rata reward amount: (distributable * stake) / totalStakeAmount.
771
- tokenAmount = mulDiv({x: ctx.distributable, y: stake, denominator: ctx.totalStakeAmount});
772
-
773
- // If the pro-rata amount rounds to zero, do not consume the owner's voting budget.
774
- if (tokenAmount == 0) return (0, newUniqueCount);
775
-
776
- // Record that this owner has consumed additional voting power from their budget.
777
- consumed[ownerIndex] += stake;
778
-
779
- // Only create a vesting entry and emit an event if there is a non-zero reward.
780
- // Push a new vesting data entry for this token ID, starting with zero shareClaimed.
781
- vestingDataOf[ctx.hook][tokenId][ctx.token].push(
782
- JBVestingData({releaseRound: ctx.vestingReleaseRound, amount: tokenAmount, shareClaimed: 0})
783
- );
784
-
785
- // Emit the claim event for off-chain indexers.
786
- emit Claimed({
787
- hook: ctx.hook,
788
- tokenId: tokenId,
789
- token: ctx.token,
790
- amount: tokenAmount,
791
- vestingReleaseRound: ctx.vestingReleaseRound,
792
- caller: msg.sender
793
- });
794
- }
795
551
  }
@@ -20,7 +20,6 @@ import {IJBDistributor} from "./interfaces/IJBDistributor.sol";
20
20
  import {JBVestingMath} from "./libraries/JBVestingMath.sol";
21
21
  import {JBBorrowContext} from "./structs/JBBorrowContext.sol";
22
22
  import {JBRewardRoundData} from "./structs/JBRewardRoundData.sol";
23
- import {JBTokenSnapshotData} from "./structs/JBTokenSnapshotData.sol";
24
23
  import {JBVestingData} from "./structs/JBVestingData.sol";
25
24
  import {JBVestingLoan} from "./structs/JBVestingLoan.sol";
26
25
 
@@ -59,9 +58,6 @@ abstract contract JBDistributor is IJBDistributor {
59
58
  /// @notice Thrown when the caller does not have access to the token.
60
59
  error JBDistributor_NoAccess(address hook, uint256 tokenId, address account);
61
60
 
62
- /// @notice Thrown when there is nothing to distribute for a token in the current round.
63
- error JBDistributor_NothingToDistribute(address hook, address token, uint256 round);
64
-
65
61
  /// @notice Thrown when there are no uncollected vesting revnet tokens to collateralize a loan.
66
62
  error JBDistributor_NothingToBorrow(address hook, address token);
67
63
 
@@ -202,21 +198,6 @@ abstract contract JBDistributor is IJBDistributor {
202
198
  /// @custom:param loanId The Revnet loan NFT ID.
203
199
  mapping(uint256 loanId => JBVestingLoan) internal _vestingLoanOf;
204
200
 
205
- /// @notice The snapshot data of the token information for each round.
206
- /// @custom:param hook The hook the snapshot is for.
207
- /// @custom:param token The address of the token claimed and vested.
208
- /// @custom:param round The round to which the data applies.
209
- mapping(address hook => mapping(IERC20 token => mapping(uint256 round => JBTokenSnapshotData snapshot))) internal
210
- _snapshotAtRoundOf;
211
-
212
- /// @notice Whether a snapshot has been taken for a given (hook, token, round).
213
- /// @dev Required because a snapshot can legitimately store `{balance: 0, vestingAmount: 0}`,
214
- /// so a zero balance is not a usable sentinel for "uninitialized".
215
- /// @custom:param hook The hook the snapshot is for.
216
- /// @custom:param token The address of the token claimed and vested.
217
- /// @custom:param round The round to which the data applies.
218
- mapping(address hook => mapping(IERC20 token => mapping(uint256 round => bool))) internal _snapshotInitializedFor;
219
-
220
201
  //*********************************************************************//
221
202
  // ------------------- transient stored properties ------------------- //
222
203
  //*********************************************************************//
@@ -271,9 +252,11 @@ abstract contract JBDistributor is IJBDistributor {
271
252
  // ---------------------- external transactions ---------------------- //
272
253
  //*********************************************************************//
273
254
 
274
- /// @notice Snapshot the current round's distributable balance and begin vesting for the specified token IDs.
275
- /// Each token ID's share is proportional to its stake weight relative to the total stake at the snapshot block.
276
- /// Vesting completes after `VESTING_ROUNDS` rounds. Reverts if there's nothing to distribute.
255
+ /// @notice Begin vesting all unclaimed past reward rounds for the specified token IDs.
256
+ /// @dev Materializes each token ID's pro-rata share of every past (non-current) reward round into fresh vesting
257
+ /// entries that start now and unlock over `VESTING_ROUNDS`. Current-round funding is excluded until a later round
258
+ /// starts. The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
259
+ /// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
277
260
  /// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
278
261
  /// @param tokenIds The staker token IDs to claim rewards for.
279
262
  /// @param tokens The reward tokens to begin vesting.
@@ -287,55 +270,18 @@ abstract contract JBDistributor is IJBDistributor {
287
270
  override
288
271
  {
289
272
  // Reward accounting cannot change while an ERC-20 `transferFrom` is in progress. A callback-capable reward
290
- // token could otherwise snapshot, vest, or collect against balances between `balanceBefore` and
291
- // `balanceAfter`, distorting the delta credited to the funder.
273
+ // token could otherwise vest or collect against balances mid-transfer, distorting the credited delta.
292
274
  _requireNotAcceptingToken();
293
275
 
294
276
  // Revert if no token IDs are provided.
295
277
  if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
296
278
 
297
- // Keep a reference to the current round.
298
- uint256 round = currentRound();
299
-
300
- // Ensure the snapshot block is recorded for this round.
301
- _ensureSnapshotBlock(round);
302
-
303
- // Keep a reference to the total staked amount at the snapshot block.
304
- uint256 totalStakeAmount = _totalStake({hook: hook, blockNumber: roundSnapshotBlock[round]});
305
-
306
- // Skip vesting when there are no stakers — funds carry over to the next round.
307
- if (totalStakeAmount == 0) return;
308
-
309
- // Loop through each token for which vesting is beginning.
310
- for (uint256 i; i < tokens.length;) {
311
- IERC20 token = tokens[i];
312
-
313
- // Take a snapshot of the token balance if it hasn't been taken already.
314
- JBTokenSnapshotData memory snapshot = _takeSnapshotOf({hook: hook, token: token});
315
- uint256 distributable = snapshot.balance - snapshot.vestingAmount;
316
-
317
- // Revert if there is nothing to distribute for this token.
318
- if (distributable == 0) {
319
- revert JBDistributor_NothingToDistribute({hook: hook, token: address(token), round: round});
320
- }
321
-
322
- // Vest each token ID and get the total amount vested.
323
- uint256 totalVestingAmount = _vestTokenIds({
324
- hook: hook,
325
- tokenIds: tokenIds,
326
- token: token,
327
- distributable: distributable,
328
- totalStakeAmount: totalStakeAmount,
329
- vestingReleaseRound: round + VESTING_ROUNDS
330
- });
331
-
332
- unchecked {
333
- // Store the updated total claimed amount now vesting.
334
- totalVestingAmountOf[hook][token] += totalVestingAmount;
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});
335
282
 
336
- ++i;
337
- }
338
- }
283
+ // Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
284
+ _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
339
285
  }
340
286
 
341
287
  /// @notice Directly fund the distributor for a specific hook by pulling tokens from the caller. An alternative
@@ -506,23 +452,6 @@ abstract contract JBDistributor is IJBDistributor {
506
452
  }
507
453
  }
508
454
 
509
- /// @notice The snapshot data of the token information for each round.
510
- /// @param hook The hook the snapshot is for.
511
- /// @param token The address of the token claimed and vested.
512
- /// @param round The round to which the data applies.
513
- function snapshotAtRoundOf(
514
- address hook,
515
- IERC20 token,
516
- uint256 round
517
- )
518
- external
519
- view
520
- override
521
- returns (JBTokenSnapshotData memory)
522
- {
523
- return _snapshotAtRoundOf[hook][token][round];
524
- }
525
-
526
455
  /// @notice The vesting position collateralized by a Revnet loan.
527
456
  /// @param loanId The Revnet loan NFT ID.
528
457
  function vestingLoanOf(uint256 loanId) external view override returns (JBVestingLoan memory) {
@@ -548,11 +477,12 @@ abstract contract JBDistributor is IJBDistributor {
548
477
  // ----------------------- public transactions ----------------------- //
549
478
  //*********************************************************************//
550
479
 
551
- /// @notice Collect tokens that have vested (partially or fully) and transfer them to the beneficiary. Also
552
- /// auto-vests for the current round if rewards haven't been claimed yet — so callers don't need to separately
553
- /// call `beginVesting`. Only the token owner (verified via `_canClaim`) can collect.
480
+ /// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
481
+ /// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
482
+ /// @dev The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
483
+ /// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
554
484
  /// @param hook The hook whose stakers are collecting.
555
- /// @param tokenIds The IDs of the tokens to collect for (caller must own all of them).
485
+ /// @param tokenIds The IDs of the tokens to collect for (caller must be authorized for all of them).
556
486
  /// @param tokens The reward tokens to collect vested amounts of.
557
487
  /// @param beneficiary The recipient of the collected tokens.
558
488
  function collectVestedRewards(
@@ -565,62 +495,20 @@ abstract contract JBDistributor is IJBDistributor {
565
495
  virtual
566
496
  override
567
497
  {
568
- // Collections transfer reward tokens out. If this runs inside the same reward token's inbound transfer, the
569
- // outgoing transfer can net against the incoming balance delta and strand the new funds unaccounted.
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.
570
500
  _requireNotAcceptingToken();
571
501
 
572
502
  // Revert if no token IDs are provided.
573
503
  if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
574
504
 
575
- // Make sure that all tokens can be claimed by this sender.
576
- for (uint256 i; i < tokenIds.length;) {
577
- if (!_canClaim({hook: hook, tokenId: tokenIds[i], account: msg.sender})) {
578
- revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
579
- }
580
- unchecked {
581
- ++i;
582
- }
583
- }
584
-
585
- // --- Auto-vest for the current round ---
586
- uint256 round = currentRound();
587
-
588
- // Ensure the snapshot block is recorded for this round.
589
- _ensureSnapshotBlock(round);
590
-
591
- // Keep a reference to the total staked amount at the snapshot block.
592
- uint256 totalStakeAmount = _totalStake({hook: hook, blockNumber: roundSnapshotBlock[round]});
593
-
594
- // Loop through each token and auto-vest if there's something distributable.
595
- for (uint256 i; i < tokens.length;) {
596
- IERC20 token = tokens[i];
597
-
598
- // Take a snapshot of the token balance if it hasn't been taken already.
599
- JBTokenSnapshotData memory snapshot = _takeSnapshotOf({hook: hook, token: token});
600
- uint256 distributable = snapshot.balance - snapshot.vestingAmount;
601
-
602
- // Only auto-vest if there's something to distribute and there's stake.
603
- if (distributable > 0 && totalStakeAmount > 0) {
604
- uint256 totalVestingAmount = _vestTokenIds({
605
- hook: hook,
606
- tokenIds: tokenIds,
607
- token: token,
608
- distributable: distributable,
609
- totalStakeAmount: totalStakeAmount,
610
- vestingReleaseRound: round + VESTING_ROUNDS
611
- });
505
+ // Only the entity authorized for these token IDs may materialize and collect their rewards.
506
+ _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
612
507
 
613
- unchecked {
614
- totalVestingAmountOf[hook][token] += totalVestingAmount;
615
- }
616
- }
508
+ // Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
509
+ _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
617
510
 
618
- unchecked {
619
- ++i;
620
- }
621
- }
622
-
623
- // Unlock the rewards and send them to the beneficiary.
511
+ // Release whatever portion of existing vesting entries has unlocked by this round.
624
512
  _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true});
625
513
  }
626
514
 
@@ -1261,41 +1149,6 @@ abstract contract JBDistributor is IJBDistributor {
1261
1149
  }
1262
1150
  }
1263
1151
 
1264
- /// @notice Takes a snapshot of the token balance and vesting amount for the current round.
1265
- /// @param hook The hook to take the snapshot for.
1266
- /// @param token The token address to take a snapshot of.
1267
- /// @return snapshot The snapshot data.
1268
- function _takeSnapshotOf(address hook, IERC20 token) internal returns (JBTokenSnapshotData memory snapshot) {
1269
- // Keep a reference to the current round.
1270
- uint256 round = currentRound();
1271
-
1272
- // If a snapshot was already taken at this round, do not take a new one. The init flag must be used as the
1273
- // sentinel: a zero balance is a valid snapshot value (round started with no funded balance), not a signal
1274
- // to re-snapshot. Re-snapshotting would let mid-round deposits leak into the current round's allocation.
1275
- if (_snapshotInitializedFor[hook][token][round]) {
1276
- return _snapshotAtRoundOf[hook][token][round];
1277
- }
1278
-
1279
- // Exclude collateralized vesting inventory because those tokens have been burned into distributor-held loans.
1280
- uint256 vestingAmount = totalVestingAmountOf[hook][token] - totalLoanedVestingAmountOf[hook][token];
1281
-
1282
- // Take a snapshot using the hook's tracked balance.
1283
- snapshot = JBTokenSnapshotData({balance: _balanceOf[hook][token], vestingAmount: vestingAmount});
1284
-
1285
- // Store the snapshot and mark it initialized.
1286
- _snapshotAtRoundOf[hook][token][round] = snapshot;
1287
- _snapshotInitializedFor[hook][token][round] = true;
1288
-
1289
- emit SnapshotCreated({
1290
- hook: hook,
1291
- round: round,
1292
- token: token,
1293
- balance: snapshot.balance,
1294
- vestingAmount: snapshot.vestingAmount,
1295
- caller: msg.sender
1296
- });
1297
- }
1298
-
1299
1152
  /// @notice The deadline for a reward round using this distributor's immutable claim duration.
1300
1153
  /// @param round The reward round.
1301
1154
  /// @return claimDeadline The deadline timestamp. Zero means no expiration.
@@ -1470,82 +1323,6 @@ abstract contract JBDistributor is IJBDistributor {
1470
1323
  }
1471
1324
  }
1472
1325
 
1473
- /// @notice Vests each token ID for a given reward token and returns the total amount vested.
1474
- /// @dev Silently skips already-vested tokenIds instead of reverting, to support auto-vest.
1475
- /// @param hook The hook whose stakers are vesting.
1476
- /// @param tokenIds The IDs to claim rewards for.
1477
- /// @param token The reward token.
1478
- /// @param distributable The distributable amount for this round.
1479
- /// @param totalStakeAmount The total stake amount.
1480
- /// @param vestingReleaseRound The round at which vesting will be released.
1481
- /// @return totalVestingAmount The total amount that began vesting.
1482
- function _vestTokenIds(
1483
- address hook,
1484
- uint256[] calldata tokenIds,
1485
- IERC20 token,
1486
- uint256 distributable,
1487
- uint256 totalStakeAmount,
1488
- uint256 vestingReleaseRound
1489
- )
1490
- internal
1491
- virtual
1492
- returns (uint256 totalVestingAmount)
1493
- {
1494
- for (uint256 j; j < tokenIds.length;) {
1495
- uint256 tokenId = tokenIds[j];
1496
-
1497
- // Skip burned tokens — they are excluded from _totalStake, so including them would overbook vesting.
1498
- if (_tokenBurned({hook: hook, tokenId: tokenId})) {
1499
- unchecked {
1500
- ++j;
1501
- }
1502
- continue;
1503
- }
1504
-
1505
- // Keep a reference to the vesting data for this hook/tokenId/token.
1506
- JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
1507
-
1508
- // Skip if this token has already been vested for this round (same releaseRound).
1509
- uint256 numVesting = vestings.length;
1510
- if (numVesting != 0 && vestings[numVesting - 1].releaseRound == vestingReleaseRound) {
1511
- unchecked {
1512
- ++j;
1513
- }
1514
- continue;
1515
- }
1516
-
1517
- // Keep a reference to the amount of tokens being claimed.
1518
- uint256 tokenAmount = mulDiv({
1519
- x: distributable, y: _tokenStake({hook: hook, tokenId: tokenId}), denominator: totalStakeAmount
1520
- });
1521
-
1522
- // Skip zero-amount entries to prevent stalling latestVestedIndexOf advancement.
1523
- if (tokenAmount == 0) {
1524
- unchecked {
1525
- ++j;
1526
- }
1527
- continue;
1528
- }
1529
-
1530
- // Add to the list of vesting data.
1531
- vestings.push(JBVestingData({releaseRound: vestingReleaseRound, amount: tokenAmount, shareClaimed: 0}));
1532
-
1533
- emit Claimed({
1534
- hook: hook,
1535
- tokenId: tokenId,
1536
- token: token,
1537
- amount: tokenAmount,
1538
- vestingReleaseRound: vestingReleaseRound,
1539
- caller: msg.sender
1540
- });
1541
-
1542
- unchecked {
1543
- totalVestingAmount += tokenAmount;
1544
- ++j;
1545
- }
1546
- }
1547
- }
1548
-
1549
1326
  //*********************************************************************//
1550
1327
  // ----------------------- internal views ---------------------------- //
1551
1328
  //*********************************************************************//
@@ -144,58 +144,10 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
144
144
  }
145
145
  }
146
146
 
147
- /// @notice Snapshot this staker's past reward rounds and start vesting them now.
148
- /// @dev Unlike the shared distributor flow, token claims are owner-initiated. This prevents third parties from
149
- /// starting a staker's vesting clock before the staker actually claims.
150
- /// @param hook The IVotes token whose stakers are vesting.
151
- /// @param tokenIds The encoded staker addresses to claim for.
152
- /// @param tokens The reward tokens to begin vesting.
153
- function beginVesting(
154
- address hook,
155
- uint256[] calldata tokenIds,
156
- IERC20[] calldata tokens
157
- )
158
- external
159
- override(JBDistributor, IJBDistributor)
160
- {
161
- // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
162
- _requireNotAcceptingToken();
163
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
164
-
165
- // Token IDs encode staker addresses, so only the encoded staker can start their own vesting clock.
166
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
167
-
168
- // Materialize all unclaimed historical rewards into fresh vesting entries that start now.
169
- _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
170
- }
171
-
172
- /// @notice Collect already-vested rewards and first start vesting any unclaimed past reward rounds.
173
- /// @param hook The IVotes token whose stakers are collecting.
174
- /// @param tokenIds The encoded staker addresses to collect for.
175
- /// @param tokens The reward tokens to collect.
176
- /// @param beneficiary The recipient of collected vested rewards.
177
- function collectVestedRewards(
178
- address hook,
179
- uint256[] calldata tokenIds,
180
- IERC20[] calldata tokens,
181
- address beneficiary
182
- )
183
- public
184
- override(JBDistributor, IJBDistributor)
185
- {
186
- // Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
187
- _requireNotAcceptingToken();
188
- if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
189
-
190
- // Only the encoded staker can materialize and collect their token rewards.
191
- _requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
192
-
193
- // Before collecting, bring the caller current by starting vesting for any past reward rounds.
194
- _claimPastRewards({hook: hook, tokenIds: tokenIds, tokens: tokens});
195
-
196
- // Release whatever portion of existing vesting entries has unlocked by this round.
197
- _unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true});
198
- }
147
+ // `beginVesting` and `collectVestedRewards` are provided by `JBDistributor`. Both distributors share the exact
148
+ // same flow (authorize -> materialize past rounds via `_claimPastRewards` -> optionally release unlocked), so the
149
+ // round-claim logic lives once in the base and dispatches to this contract's `_claimPastRewards` /
150
+ // `_requireCanClaimTokenIds` overrides below.
199
151
 
200
152
  //*********************************************************************//
201
153
  // -------------------------- public views --------------------------- //
@@ -6,7 +6,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
6
  import {IREVLoans} from "@rev-net/core-v6/src/interfaces/IREVLoans.sol";
7
7
  import {IREVOwner} from "@rev-net/core-v6/src/interfaces/IREVOwner.sol";
8
8
 
9
- import {JBTokenSnapshotData} from "../structs/JBTokenSnapshotData.sol";
10
9
  import {JBVestingLoan} from "../structs/JBVestingLoan.sol";
11
10
 
12
11
  /// @notice Interface for round-based reward distributors with linear vesting. Stakers claim their share of funded
@@ -139,22 +138,6 @@ interface IJBDistributor {
139
138
  address caller
140
139
  );
141
140
 
142
- /// @notice Emitted when a snapshot is created for a round.
143
- /// @param hook The hook the snapshot is for.
144
- /// @param round The round the snapshot was created for.
145
- /// @param token The token the snapshot is of.
146
- /// @param balance The token balance at the time of the snapshot.
147
- /// @param vestingAmount The amount of tokens vesting at the time of the snapshot.
148
- /// @param caller The address that triggered the snapshot.
149
- event SnapshotCreated(
150
- address indexed hook,
151
- uint256 indexed round,
152
- IERC20 indexed token,
153
- uint256 balance,
154
- uint256 vestingAmount,
155
- address caller
156
- );
157
-
158
141
  //*********************************************************************//
159
142
  // ----------------------------- views ------------------------------- //
160
143
  //*********************************************************************//
@@ -216,19 +199,6 @@ interface IJBDistributor {
216
199
  /// @param round The round to get the start timestamp of.
217
200
  function roundStartTimestamp(uint256 round) external view returns (uint256);
218
201
 
219
- /// @notice The snapshot data of the token information for each round.
220
- /// @param hook The hook the snapshot is for.
221
- /// @param token The address of the token to check.
222
- /// @param round The round to which the data applies.
223
- function snapshotAtRoundOf(
224
- address hook,
225
- IERC20 token,
226
- uint256 round
227
- )
228
- external
229
- view
230
- returns (JBTokenSnapshotData memory);
231
-
232
202
  /// @notice The amount of a token that is currently vesting for a hook's stakers.
233
203
  /// @param hook The hook whose vesting amount to check.
234
204
  /// @param token The address of the token that is vesting.
@@ -1,11 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.0;
3
-
4
- /// @notice A point-in-time snapshot of a reward token's state for a specific hook and round. The distributable
5
- /// amount for the round is `balance - vestingAmount`.
6
- /// @custom:member balance The total token balance held for the hook's stakers at snapshot time.
7
- /// @custom:member vestingAmount The amount currently locked in vesting at snapshot time (not yet distributable).
8
- struct JBTokenSnapshotData {
9
- uint256 balance;
10
- uint256 vestingAmount;
11
- }