@bananapus/distributor-v6 0.0.3 → 0.0.5

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.
@@ -13,45 +13,56 @@ import {JBVestingData} from "./structs/JBVestingData.sol";
13
13
  /// @notice A contract managing distributions of tokens to be claimed and vested by stakers of any other token.
14
14
  abstract contract JBDistributor is IJBDistributor {
15
15
  using SafeERC20 for IERC20;
16
+
16
17
  //*********************************************************************//
17
18
  // --------------------------- custom errors ------------------------- //
18
19
  //*********************************************************************//
19
20
 
20
- /// @notice Thrown when a token has already begun vesting for a given round.
21
- error JBDistributor_AlreadyVesting();
21
+ /// @notice Thrown when an empty tokenIds array is passed.
22
+ error JBDistributor_EmptyTokenIds();
22
23
 
23
24
  /// @notice Thrown when a native ETH transfer fails.
24
25
  error JBDistributor_NativeTransferFailed();
25
26
 
27
+ /// @notice Thrown when the caller does not have access to the token.
28
+ error JBDistributor_NoAccess();
29
+
26
30
  /// @notice Thrown when there is nothing to distribute for a token in the current round.
27
31
  error JBDistributor_NothingToDistribute();
28
32
 
29
- /// @notice Thrown when the caller does not have access to the token.
30
- error JBDistributor_NoAccess();
33
+ //*********************************************************************//
34
+ // ------------------------- public constants ------------------------ //
35
+ //*********************************************************************//
36
+
37
+ /// @notice The number of shares that represent 100%.
38
+ uint256 public constant MAX_SHARE = 100_000;
31
39
 
32
40
  //*********************************************************************//
33
41
  // ---------------- public immutable stored properties --------------- //
34
42
  //*********************************************************************//
35
43
 
36
- /// @notice The starting block of the distributor.
37
- uint256 public immutable startingBlock;
38
-
39
- /// @notice The minimum amount of time stakers have to claim rewards, specified in blocks.
44
+ /// @notice The duration of each round, specified in seconds.
40
45
  uint256 public immutable override roundDuration;
41
46
 
47
+ /// @notice The starting timestamp of the distributor.
48
+ uint256 public immutable startingTimestamp;
49
+
42
50
  /// @notice The number of rounds until tokens are fully vested.
43
51
  uint256 public immutable override vestingRounds;
44
52
 
45
53
  //*********************************************************************//
46
- // --------------------- public constant properties ----------------- //
54
+ // --------------------- public stored properties -------------------- //
47
55
  //*********************************************************************//
48
56
 
49
- /// @notice The number of shares that represent 100%.
50
- uint256 public constant MAX_SHARE = 100_000;
57
+ /// @notice The index within `vestingDataOf` of the latest vest.
58
+ /// @custom:param hook The hook the tokenId belongs to.
59
+ /// @custom:param tokenId The ID of the token to which the vests belong.
60
+ /// @custom:param token The address of the token being vested.
61
+ mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256))) public latestVestedIndexOf;
51
62
 
52
- //*********************************************************************//
53
- // --------------------- public stored properties -------------------- //
54
- //*********************************************************************//
63
+ /// @notice The block number recorded as the snapshot point for each round.
64
+ /// @dev Set to `block.number - 1` on first interaction in a round, so that `IVotes.getPastVotes` works.
65
+ mapping(uint256 round => uint256) public override roundSnapshotBlock;
55
66
 
56
67
  /// @notice The amount of a token that is currently vesting for a hook's stakers.
57
68
  /// @custom:param hook The hook whose stakers are vesting.
@@ -65,14 +76,8 @@ abstract contract JBDistributor is IJBDistributor {
65
76
  // slither-disable-next-line uninitialized-state
66
77
  mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => JBVestingData[]))) public vestingDataOf;
67
78
 
68
- /// @notice The index within `vestingDataOf` of the latest vest.
69
- /// @custom:param hook The hook the tokenId belongs to.
70
- /// @custom:param tokenId The ID of the token to which the vests belong.
71
- /// @custom:param token The address of the token being vested.
72
- mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256))) public latestVestedIndexOf;
73
-
74
79
  //*********************************************************************//
75
- // ------------------------ internal properties ---------------------- //
80
+ // -------------------- internal stored properties ------------------- //
76
81
  //*********************************************************************//
77
82
 
78
83
  /// @notice The balance of a token held for a specific hook's stakers.
@@ -88,42 +93,123 @@ abstract contract JBDistributor is IJBDistributor {
88
93
  _snapshotAtRoundOf;
89
94
 
90
95
  //*********************************************************************//
91
- // -------------------------- public views --------------------------- //
96
+ // -------------------------- constructor ---------------------------- //
92
97
  //*********************************************************************//
93
98
 
94
- /// @notice The number of the current round.
95
- function currentRound() public view override returns (uint256) {
96
- return (block.number - startingBlock) / roundDuration;
99
+ /// @param roundDuration_ The duration of each round, specified in seconds.
100
+ /// @param vestingRounds_ The number of rounds until tokens are fully vested.
101
+ constructor(uint256 roundDuration_, uint256 vestingRounds_) {
102
+ startingTimestamp = block.timestamp;
103
+ roundDuration = roundDuration_;
104
+ vestingRounds = vestingRounds_;
105
+ }
106
+
107
+ //*********************************************************************//
108
+ // ---------------------- external transactions ---------------------- //
109
+ //*********************************************************************//
110
+
111
+ /// @notice Claims tokens and begins vesting.
112
+ /// @param hook The hook whose stakers are vesting.
113
+ /// @param tokenIds The IDs to claim rewards for.
114
+ /// @param tokens The tokens to claim.
115
+ function beginVesting(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) external override {
116
+ // Revert if no token IDs are provided.
117
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds();
118
+
119
+ // Keep a reference to the current round.
120
+ uint256 round = currentRound();
121
+
122
+ // Ensure the snapshot block is recorded for this round.
123
+ _ensureSnapshotBlock(round);
124
+
125
+ // Keep a reference to the total staked amount at the snapshot block.
126
+ uint256 totalStakeAmount = _totalStake(hook, roundSnapshotBlock[round]);
127
+
128
+ // Skip vesting when there are no stakers — funds carry over to the next round.
129
+ // slither-disable-next-line incorrect-equality
130
+ if (totalStakeAmount == 0) return;
131
+
132
+ // Loop through each token for which vesting is beginning.
133
+ for (uint256 i; i < tokens.length;) {
134
+ IERC20 token = tokens[i];
135
+
136
+ // Take a snapshot of the token balance if it hasn't been taken already.
137
+ JBTokenSnapshotData memory snapshot = _takeSnapshotOf(hook, token);
138
+ uint256 distributable = snapshot.balance - snapshot.vestingAmount;
139
+
140
+ // Revert if there is nothing to distribute for this token.
141
+ if (distributable == 0) revert JBDistributor_NothingToDistribute();
142
+
143
+ // Vest each token ID and get the total amount vested.
144
+ uint256 totalVestingAmount =
145
+ _vestTokenIds(hook, tokenIds, token, distributable, totalStakeAmount, round + vestingRounds);
146
+
147
+ unchecked {
148
+ // Store the updated total claimed amount now vesting.
149
+ totalVestingAmountOf[hook][token] += totalVestingAmount;
150
+
151
+ ++i;
152
+ }
153
+ }
97
154
  }
98
155
 
99
- /// @notice The block at which a round started.
100
- /// @param round The round to get the start block of.
101
- function roundStartBlock(uint256 round) public view override returns (uint256) {
102
- return startingBlock + roundDuration * round;
156
+ /// @notice Fund the distributor for a specific hook by pulling tokens from the caller.
157
+ /// @dev For native ETH, send `msg.value` and pass `IERC20(JBConstants.NATIVE_TOKEN)` as the token.
158
+ /// @param hook The hook to fund.
159
+ /// @param token The token to fund with.
160
+ /// @param amount The amount to fund.
161
+ function fund(address hook, IERC20 token, uint256 amount) external payable override {
162
+ if (address(token) == JBConstants.NATIVE_TOKEN) {
163
+ amount = msg.value;
164
+ } else {
165
+ // Use balance delta to handle fee-on-transfer tokens correctly.
166
+ uint256 balanceBefore = token.balanceOf(address(this));
167
+ token.safeTransferFrom(msg.sender, address(this), amount);
168
+ amount = token.balanceOf(address(this)) - balanceBefore;
169
+ }
170
+ _balanceOf[hook][token] += amount;
103
171
  }
104
172
 
105
- /// @notice The balance of a token held for a specific hook's stakers.
106
- /// @param hook The hook whose balance to check.
107
- /// @param token The token to check the balance of.
108
- function balanceOf(address hook, IERC20 token) external view override returns (uint256) {
109
- return _balanceOf[hook][token];
173
+ /// @notice Record the snapshot block for the current round. Callable by anyone (keepers, frontends).
174
+ function poke() external override {
175
+ _ensureSnapshotBlock(currentRound());
110
176
  }
111
177
 
112
- /// @notice The snapshot data of the token information for each round.
113
- /// @param hook The hook the snapshot is for.
114
- /// @param token The address of the token being claimed and vested.
115
- /// @param round The round to which the data applies.
116
- function snapshotAtRoundOf(
178
+ /// @notice Release vested rewards in the case that a token was burned.
179
+ /// @param hook The hook whose tokens were burned.
180
+ /// @param tokenIds The IDs of the burned tokens.
181
+ /// @param tokens The address of the tokens being released.
182
+ /// @param beneficiary The recipient of the released tokens.
183
+ function releaseForfeitedRewards(
117
184
  address hook,
118
- IERC20 token,
119
- uint256 round
185
+ uint256[] calldata tokenIds,
186
+ IERC20[] calldata tokens,
187
+ address beneficiary
120
188
  )
121
189
  external
122
- view
123
190
  override
124
- returns (JBTokenSnapshotData memory)
125
191
  {
126
- return _snapshotAtRoundOf[hook][token][round];
192
+ // Make sure that all tokens are burned.
193
+ for (uint256 i; i < tokenIds.length;) {
194
+ if (!_tokenBurned(hook, tokenIds[i])) revert JBDistributor_NoAccess();
195
+ unchecked {
196
+ ++i;
197
+ }
198
+ }
199
+
200
+ // Unlock the rewards and send them to the beneficiary.
201
+ _unlockRewards(hook, tokenIds, tokens, beneficiary, false);
202
+ }
203
+
204
+ //*********************************************************************//
205
+ // ----------------------- external views ---------------------------- //
206
+ //*********************************************************************//
207
+
208
+ /// @notice The balance of a token held for a specific hook's stakers.
209
+ /// @param hook The hook whose balance to check.
210
+ /// @param token The token to check the balance of.
211
+ function balanceOf(address hook, IERC20 token) external view override returns (uint256) {
212
+ return _balanceOf[hook][token];
127
213
  }
128
214
 
129
215
  /// @notice Calculate how much of the token has been claimed for the given tokenId.
@@ -202,161 +288,43 @@ abstract contract JBDistributor is IJBDistributor {
202
288
  }
203
289
  }
204
290
 
205
- //*********************************************************************//
206
- // -------------------------- constructor ---------------------------- //
207
- //*********************************************************************//
208
-
209
- /// @param roundDuration_ The minimum amount of time stakers have to claim rewards, specified in blocks. Make sure
210
- /// this is correct for each blockchain/rollup this gets deployed to.
211
- /// @param vestingRounds_ The number of rounds until tokens are fully vested.
212
- constructor(uint256 roundDuration_, uint256 vestingRounds_) {
213
- startingBlock = block.number;
214
- roundDuration = roundDuration_;
215
- vestingRounds = vestingRounds_;
216
- }
217
-
218
- //*********************************************************************//
219
- // ---------------------- external transactions ---------------------- //
220
- //*********************************************************************//
221
-
222
- /// @notice Fund the distributor for a specific hook by pulling tokens from the caller.
223
- /// @dev For native ETH, send `msg.value` and pass `IERC20(JBConstants.NATIVE_TOKEN)` as the token.
224
- /// @param hook The hook to fund.
225
- /// @param token The token to fund with.
226
- /// @param amount The amount to fund.
227
- function fund(address hook, IERC20 token, uint256 amount) external payable override {
228
- if (address(token) == JBConstants.NATIVE_TOKEN) {
229
- amount = msg.value;
230
- } else {
231
- // Use balance delta to handle fee-on-transfer tokens correctly.
232
- uint256 balanceBefore = token.balanceOf(address(this));
233
- token.safeTransferFrom(msg.sender, address(this), amount);
234
- amount = token.balanceOf(address(this)) - balanceBefore;
235
- }
236
- _balanceOf[hook][token] += amount;
237
- }
238
-
239
- /// @notice Claims tokens and begins vesting.
240
- /// @param hook The hook whose stakers are vesting.
241
- /// @param tokenIds The IDs to claim rewards for.
242
- /// @param tokens The tokens to claim.
243
- function beginVesting(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) external override {
244
- // Keep a reference to the current round.
245
- uint256 round = currentRound();
246
-
247
- // Keep a reference to the total staked amount at the current round.
248
- uint256 totalStakeAmount = _totalStake(hook, roundStartBlock(round));
249
-
250
- // Loop through each token for which vesting is beginning.
251
- for (uint256 i; i < tokens.length;) {
252
- IERC20 token = tokens[i];
253
-
254
- // Take a snapshot of the token balance if it hasn't been taken already.
255
- JBTokenSnapshotData memory snapshot = _takeSnapshotOf(hook, token);
256
- uint256 distributable = snapshot.balance - snapshot.vestingAmount;
257
-
258
- // Revert if there is nothing to distribute for this token.
259
- if (distributable == 0) revert JBDistributor_NothingToDistribute();
260
-
261
- // Vest each token ID and get the total amount vested.
262
- uint256 totalVestingAmount =
263
- _vestTokenIds(hook, tokenIds, token, distributable, totalStakeAmount, round + vestingRounds);
264
-
265
- unchecked {
266
- // Store the updated total claimed amount now vesting.
267
- totalVestingAmountOf[hook][token] += totalVestingAmount;
268
-
269
- ++i;
270
- }
271
- }
272
- }
273
-
274
- /// @notice Vests each token ID for a given reward token and returns the total amount vested.
275
- /// @param hook The hook whose stakers are vesting.
276
- /// @param tokenIds The IDs to claim rewards for.
277
- /// @param token The reward token.
278
- /// @param distributable The distributable amount for this round.
279
- /// @param totalStakeAmount The total stake amount.
280
- /// @param vestingReleaseRound The round at which vesting will be released.
281
- /// @return totalVestingAmount The total amount that began vesting.
282
- function _vestTokenIds(
291
+ /// @notice The snapshot data of the token information for each round.
292
+ /// @param hook The hook the snapshot is for.
293
+ /// @param token The address of the token being claimed and vested.
294
+ /// @param round The round to which the data applies.
295
+ function snapshotAtRoundOf(
283
296
  address hook,
284
- uint256[] calldata tokenIds,
285
297
  IERC20 token,
286
- uint256 distributable,
287
- uint256 totalStakeAmount,
288
- uint256 vestingReleaseRound
298
+ uint256 round
289
299
  )
290
- internal
291
- returns (uint256 totalVestingAmount)
300
+ external
301
+ view
302
+ override
303
+ returns (JBTokenSnapshotData memory)
292
304
  {
293
- for (uint256 j; j < tokenIds.length;) {
294
- uint256 tokenId = tokenIds[j];
295
-
296
- // Skip burned tokens — they are excluded from _totalStake, so including them would overbook vesting.
297
- if (_tokenBurned(hook, tokenId)) {
298
- unchecked {
299
- ++j;
300
- }
301
- continue;
302
- }
303
-
304
- // Keep a reference to the vesting data for this hook/tokenId/token.
305
- JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
306
-
307
- // Make sure this token hasn't already been claimed by checking if the last item is the current round.
308
- uint256 numVesting = vestings.length;
309
- // slither-disable-next-line incorrect-equality
310
- if (numVesting != 0 && vestings[numVesting - 1].releaseRound == vestingReleaseRound) {
311
- revert JBDistributor_AlreadyVesting();
312
- }
313
-
314
- // Keep a reference to the amount of tokens being claimed.
315
- uint256 tokenAmount = mulDiv(distributable, _tokenStake(hook, tokenId), totalStakeAmount);
316
-
317
- // Add to the list of vesting data.
318
- vestings.push(JBVestingData({releaseRound: vestingReleaseRound, amount: tokenAmount, shareClaimed: 0}));
305
+ return _snapshotAtRoundOf[hook][token][round];
306
+ }
319
307
 
320
- emit Claimed(hook, tokenId, token, tokenAmount, vestingReleaseRound);
308
+ //*********************************************************************//
309
+ // -------------------------- public views --------------------------- //
310
+ //*********************************************************************//
321
311
 
322
- unchecked {
323
- totalVestingAmount += tokenAmount;
324
- ++j;
325
- }
326
- }
312
+ /// @notice The number of the current round.
313
+ function currentRound() public view override returns (uint256) {
314
+ return (block.timestamp - startingTimestamp) / roundDuration;
327
315
  }
328
316
 
329
- /// @notice Release vested rewards in the case that a token was burned.
330
- /// @param hook The hook whose tokens were burned.
331
- /// @param tokenIds The IDs of the burned tokens.
332
- /// @param tokens The address of the tokens being released.
333
- /// @param beneficiary The recipient of the released tokens.
334
- function releaseForfeitedRewards(
335
- address hook,
336
- uint256[] calldata tokenIds,
337
- IERC20[] calldata tokens,
338
- address beneficiary
339
- )
340
- external
341
- override
342
- {
343
- // Make sure that all tokens are burned.
344
- for (uint256 i; i < tokenIds.length;) {
345
- if (!_tokenBurned(hook, tokenIds[i])) revert JBDistributor_NoAccess();
346
- unchecked {
347
- ++i;
348
- }
349
- }
350
-
351
- // Unlock the rewards and send them to the beneficiary.
352
- _unlockRewards(hook, tokenIds, tokens, beneficiary, false);
317
+ /// @notice The timestamp at which a round started.
318
+ /// @param round The round to get the start timestamp of.
319
+ function roundStartTimestamp(uint256 round) public view override returns (uint256) {
320
+ return startingTimestamp + roundDuration * round;
353
321
  }
354
322
 
355
323
  //*********************************************************************//
356
324
  // ----------------------- public transactions ----------------------- //
357
325
  //*********************************************************************//
358
326
 
359
- /// @notice Collect vested tokens.
327
+ /// @notice Collect vested tokens. Auto-vests for the current round if not already vested.
360
328
  /// @param hook The hook whose stakers are collecting.
361
329
  /// @param tokenIds The IDs of the tokens to collect for.
362
330
  /// @param tokens The address of the tokens being claimed.
@@ -370,6 +338,9 @@ abstract contract JBDistributor is IJBDistributor {
370
338
  public
371
339
  override
372
340
  {
341
+ // Revert if no token IDs are provided.
342
+ if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds();
343
+
373
344
  // Make sure that all tokens can be claimed by this sender.
374
345
  for (uint256 i; i < tokenIds.length;) {
375
346
  if (!_canClaim(hook, tokenIds[i], msg.sender)) revert JBDistributor_NoAccess();
@@ -378,6 +349,38 @@ abstract contract JBDistributor is IJBDistributor {
378
349
  }
379
350
  }
380
351
 
352
+ // --- Auto-vest for the current round ---
353
+ uint256 round = currentRound();
354
+
355
+ // Ensure the snapshot block is recorded for this round.
356
+ _ensureSnapshotBlock(round);
357
+
358
+ // Keep a reference to the total staked amount at the snapshot block.
359
+ uint256 totalStakeAmount = _totalStake(hook, roundSnapshotBlock[round]);
360
+
361
+ // Loop through each token and auto-vest if there's something distributable.
362
+ for (uint256 i; i < tokens.length;) {
363
+ IERC20 token = tokens[i];
364
+
365
+ // Take a snapshot of the token balance if it hasn't been taken already.
366
+ JBTokenSnapshotData memory snapshot = _takeSnapshotOf(hook, token);
367
+ uint256 distributable = snapshot.balance - snapshot.vestingAmount;
368
+
369
+ // Only auto-vest if there's something to distribute and there's stake.
370
+ if (distributable > 0 && totalStakeAmount > 0) {
371
+ uint256 totalVestingAmount =
372
+ _vestTokenIds(hook, tokenIds, token, distributable, totalStakeAmount, round + vestingRounds);
373
+
374
+ unchecked {
375
+ totalVestingAmountOf[hook][token] += totalVestingAmount;
376
+ }
377
+ }
378
+
379
+ unchecked {
380
+ ++i;
381
+ }
382
+ }
383
+
381
384
  // Unlock the rewards and send them to the beneficiary.
382
385
  _unlockRewards(hook, tokenIds, tokens, beneficiary, true);
383
386
  }
@@ -386,6 +389,47 @@ abstract contract JBDistributor is IJBDistributor {
386
389
  // ---------------------- internal transactions ---------------------- //
387
390
  //*********************************************************************//
388
391
 
392
+ /// @notice Ensures that a snapshot block is recorded for the given round.
393
+ /// @dev Uses `block.number - 1` because `IVotes.getPastVotes` requires a strictly past block.
394
+ /// @param round The round to ensure a snapshot block for.
395
+ function _ensureSnapshotBlock(uint256 round) internal {
396
+ // slither-disable-next-line incorrect-equality
397
+ if (roundSnapshotBlock[round] == 0) {
398
+ roundSnapshotBlock[round] = block.number - 1;
399
+ emit RoundSnapshotRecorded(round, block.number - 1);
400
+ }
401
+ // Eagerly lock the next round's snapshot to prevent first-caller manipulation.
402
+ // slither-disable-next-line incorrect-equality
403
+ if (roundSnapshotBlock[round + 1] == 0) {
404
+ roundSnapshotBlock[round + 1] = block.number - 1;
405
+ emit RoundSnapshotRecorded(round + 1, block.number - 1);
406
+ }
407
+ }
408
+
409
+ /// @notice Takes a snapshot of the token balance and vesting amount for the current round.
410
+ /// @param hook The hook to take the snapshot for.
411
+ /// @param token The token address to take a snapshot of.
412
+ /// @return snapshot The snapshot data.
413
+ function _takeSnapshotOf(address hook, IERC20 token) internal returns (JBTokenSnapshotData memory snapshot) {
414
+ // Keep a reference to the current round.
415
+ uint256 round = currentRound();
416
+
417
+ // Keep a reference to the token's snapshot.
418
+ snapshot = _snapshotAtRoundOf[hook][token][round];
419
+
420
+ // If a snapshot was already taken at this cycle, do not take a new one.
421
+ if (snapshot.balance != 0) return snapshot;
422
+
423
+ // Take a snapshot using the hook's tracked balance.
424
+ snapshot =
425
+ JBTokenSnapshotData({balance: _balanceOf[hook][token], vestingAmount: totalVestingAmountOf[hook][token]});
426
+
427
+ // Store the snapshot.
428
+ _snapshotAtRoundOf[hook][token][round] = snapshot;
429
+
430
+ emit SnapshotCreated(hook, round, token, snapshot.balance, snapshot.vestingAmount);
431
+ }
432
+
389
433
  /// @notice Unlocks rewards for the given token IDs and tokens, either for collection or forfeiture.
390
434
  /// @param hook The hook the tokens belong to.
391
435
  /// @param tokenIds The IDs of the tokens to unlock rewards for.
@@ -508,32 +552,68 @@ abstract contract JBDistributor is IJBDistributor {
508
552
  }
509
553
  }
510
554
 
511
- /// @notice Takes a snapshot of the token balance and vesting amount for the current round.
512
- /// @param hook The hook to take the snapshot for.
513
- /// @param token The token address to take a snapshot of.
514
- /// @return snapshot The snapshot data.
515
- function _takeSnapshotOf(address hook, IERC20 token) internal returns (JBTokenSnapshotData memory snapshot) {
516
- // Keep a reference to the current round.
517
- uint256 round = currentRound();
555
+ /// @notice Vests each token ID for a given reward token and returns the total amount vested.
556
+ /// @dev Silently skips already-vested tokenIds instead of reverting, to support auto-vest.
557
+ /// @param hook The hook whose stakers are vesting.
558
+ /// @param tokenIds The IDs to claim rewards for.
559
+ /// @param token The reward token.
560
+ /// @param distributable The distributable amount for this round.
561
+ /// @param totalStakeAmount The total stake amount.
562
+ /// @param vestingReleaseRound The round at which vesting will be released.
563
+ /// @return totalVestingAmount The total amount that began vesting.
564
+ function _vestTokenIds(
565
+ address hook,
566
+ uint256[] calldata tokenIds,
567
+ IERC20 token,
568
+ uint256 distributable,
569
+ uint256 totalStakeAmount,
570
+ uint256 vestingReleaseRound
571
+ )
572
+ internal
573
+ virtual
574
+ returns (uint256 totalVestingAmount)
575
+ {
576
+ for (uint256 j; j < tokenIds.length;) {
577
+ uint256 tokenId = tokenIds[j];
518
578
 
519
- // Keep a reference to the token's snapshot.
520
- snapshot = _snapshotAtRoundOf[hook][token][round];
579
+ // Skip burned tokens they are excluded from _totalStake, so including them would overbook vesting.
580
+ if (_tokenBurned(hook, tokenId)) {
581
+ unchecked {
582
+ ++j;
583
+ }
584
+ continue;
585
+ }
521
586
 
522
- // If a snapshot was already taken at this cycle, do not take a new one.
523
- if (snapshot.balance != 0) return snapshot;
587
+ // Keep a reference to the vesting data for this hook/tokenId/token.
588
+ JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
524
589
 
525
- // Take a snapshot using the hook's tracked balance.
526
- snapshot =
527
- JBTokenSnapshotData({balance: _balanceOf[hook][token], vestingAmount: totalVestingAmountOf[hook][token]});
590
+ // Skip if this token has already been vested for this round (same releaseRound).
591
+ uint256 numVesting = vestings.length;
592
+ // slither-disable-next-line incorrect-equality
593
+ if (numVesting != 0 && vestings[numVesting - 1].releaseRound == vestingReleaseRound) {
594
+ unchecked {
595
+ ++j;
596
+ }
597
+ continue;
598
+ }
528
599
 
529
- // Store the snapshot.
530
- _snapshotAtRoundOf[hook][token][round] = snapshot;
600
+ // Keep a reference to the amount of tokens being claimed.
601
+ uint256 tokenAmount = mulDiv(distributable, _tokenStake(hook, tokenId), totalStakeAmount);
531
602
 
532
- emit SnapshotCreated(hook, round, token, snapshot.balance, snapshot.vestingAmount);
603
+ // Add to the list of vesting data.
604
+ vestings.push(JBVestingData({releaseRound: vestingReleaseRound, amount: tokenAmount, shareClaimed: 0}));
605
+
606
+ emit Claimed(hook, tokenId, token, tokenAmount, vestingReleaseRound);
607
+
608
+ unchecked {
609
+ totalVestingAmount += tokenAmount;
610
+ ++j;
611
+ }
612
+ }
533
613
  }
534
614
 
535
615
  //*********************************************************************//
536
- // ----------------------- virtual transactions ---------------------- //
616
+ // ----------------------- internal views ---------------------------- //
537
617
  //*********************************************************************//
538
618
 
539
619
  /// @notice A flag indicating if an account can currently claim their tokens.
@@ -543,11 +623,11 @@ abstract contract JBDistributor is IJBDistributor {
543
623
  /// @return canClaim A flag indicating if claiming is allowed.
544
624
  function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
545
625
 
546
- /// @notice The total amount staked at the given block.
547
- /// @param hook The hook to get the total stake for.
548
- /// @param blockNumber The block number to get the total staked amount at.
549
- /// @return totalStakedAmount The total amount staked at a block number.
550
- function _totalStake(address hook, uint256 blockNumber) internal view virtual returns (uint256 totalStakedAmount);
626
+ /// @notice Checks if the given token was burned or not.
627
+ /// @param hook The hook the token belongs to.
628
+ /// @param tokenId The tokenId to check.
629
+ /// @return tokenWasBurned A boolean that is true if the token was burned.
630
+ function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
551
631
 
552
632
  /// @notice The amount of tokens staked for the given token ID.
553
633
  /// @param hook The hook the token belongs to.
@@ -555,9 +635,9 @@ abstract contract JBDistributor is IJBDistributor {
555
635
  /// @return tokenStakeAmount The amount of staked tokens that is being represented by the token.
556
636
  function _tokenStake(address hook, uint256 tokenId) internal view virtual returns (uint256 tokenStakeAmount);
557
637
 
558
- /// @notice Checks if the given token was burned or not.
559
- /// @param hook The hook the token belongs to.
560
- /// @param tokenId The tokenId to check.
561
- /// @return tokenWasBurned A boolean that is true if the token was burned.
562
- function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
638
+ /// @notice The total amount staked at the given block.
639
+ /// @param hook The hook to get the total stake for.
640
+ /// @param blockNumber The block number to get the total staked amount at.
641
+ /// @return totalStakedAmount The total amount staked at a block number.
642
+ function _totalStake(address hook, uint256 blockNumber) internal view virtual returns (uint256 totalStakedAmount);
563
643
  }