@bananapus/distributor-v6 0.0.34 → 0.0.36
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/README.md +7 -0
- package/package.json +2 -2
- package/references/operations.md +2 -1
- package/src/JB721Distributor.sol +357 -26
- package/src/JBDistributor.sol +369 -183
- package/src/JBTokenDistributor.sol +49 -17
- package/src/interfaces/IJB721Distributor.sol +139 -0
- package/src/interfaces/IJBDistributor.sol +22 -8
- package/src/structs/JBBorrowContext.sol +2 -0
- package/src/structs/JBClaimContext.sol +5 -0
- package/src/structs/JBVestContext.sol +5 -0
- package/src/structs/JBVestingLoan.sol +2 -0
package/src/JBDistributor.sol
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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.
|
|
@@ -611,15 +507,18 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
611
507
|
// Measure any returned project tokens while excluding any source-token payment effects.
|
|
612
508
|
uint256 rewardBalanceBefore = vestingLoan.token.balanceOf(address(this));
|
|
613
509
|
|
|
614
|
-
// Repay through this distributor because it owns the loan NFT and must receive the returned collateral.
|
|
615
|
-
|
|
510
|
+
// Repay through this distributor because it owns the loan NFT and must receive the returned collateral. Any
|
|
511
|
+
// native overpayment is reported back so it can be refunded only after this loan's state is fully settled.
|
|
512
|
+
uint256 nativeRefundAmount;
|
|
513
|
+
(paidOffLoanId, nativeRefundAmount) = _repayLoanSource({
|
|
616
514
|
loanId: loanId,
|
|
617
515
|
loan: loan,
|
|
618
516
|
repayBorrowAmount: repayBorrowAmount,
|
|
619
517
|
collateralCount: vestingLoan.collateralCount
|
|
620
518
|
});
|
|
621
519
|
|
|
622
|
-
// Restore the collateral to inventory while preserving the original vesting data untouched.
|
|
520
|
+
// Restore the collateral to inventory while preserving the original vesting data untouched. This deletes the
|
|
521
|
+
// loan record and decrements the loaned-vesting inventory before any value leaves the contract.
|
|
623
522
|
_restoreVestingCollateral({
|
|
624
523
|
loanId: loanId,
|
|
625
524
|
paidOffLoanId: paidOffLoanId,
|
|
@@ -627,6 +526,15 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
627
526
|
rewardBalanceBefore: rewardBalanceBefore,
|
|
628
527
|
repayBorrowAmount: repayBorrowAmount
|
|
629
528
|
});
|
|
529
|
+
|
|
530
|
+
// Return any native overpayment last, following checks-effects-interactions. The loan is already settled, so a
|
|
531
|
+
// re-entrant call during this transfer cannot observe a half-settled loan.
|
|
532
|
+
if (nativeRefundAmount != 0) {
|
|
533
|
+
(bool success,) = msg.sender.call{value: nativeRefundAmount}("");
|
|
534
|
+
if (!success) {
|
|
535
|
+
revert JBDistributor_NativeTransferFailed({beneficiary: msg.sender, amount: nativeRefundAmount});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
630
538
|
}
|
|
631
539
|
|
|
632
540
|
/// @notice Write off a distributor-held Revnet loan after Revnet liquidation permanently destroys its collateral.
|
|
@@ -655,15 +563,210 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
655
563
|
|
|
656
564
|
/// @notice Claim all past reward rounds for the given token IDs and reward tokens into fresh vesting entries.
|
|
657
565
|
/// @param hook The hook whose stakers are claiming.
|
|
566
|
+
/// @param groupId The reward group being claimed (0 = the default group).
|
|
658
567
|
/// @param tokenIds The token IDs to claim for.
|
|
659
568
|
/// @param tokens The reward tokens to claim.
|
|
660
|
-
function _claimPastRewards(
|
|
569
|
+
function _claimPastRewards(
|
|
570
|
+
address hook,
|
|
571
|
+
uint256 groupId,
|
|
572
|
+
uint256[] calldata tokenIds,
|
|
573
|
+
IERC20[] calldata tokens
|
|
574
|
+
)
|
|
575
|
+
internal
|
|
576
|
+
virtual;
|
|
661
577
|
|
|
662
578
|
/// @notice Revert unless the caller is authorized to claim each token ID.
|
|
663
579
|
/// @param hook The hook whose token IDs are being checked.
|
|
664
580
|
/// @param tokenIds The token IDs to check.
|
|
665
581
|
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
|
|
666
582
|
|
|
583
|
+
/// @notice Shared begin-vesting logic across reward groups.
|
|
584
|
+
/// @param hook The hook whose stakers are vesting.
|
|
585
|
+
/// @param groupId The reward group (0 = the default group).
|
|
586
|
+
/// @param tokenIds The staker token IDs to claim rewards for.
|
|
587
|
+
/// @param tokens The reward tokens to begin vesting.
|
|
588
|
+
function _beginVesting(
|
|
589
|
+
address hook,
|
|
590
|
+
uint256 groupId,
|
|
591
|
+
uint256[] calldata tokenIds,
|
|
592
|
+
IERC20[] calldata tokens
|
|
593
|
+
)
|
|
594
|
+
internal
|
|
595
|
+
{
|
|
596
|
+
// Reward accounting cannot change while an ERC-20 `transferFrom` is in progress.
|
|
597
|
+
_requireNotAcceptingToken();
|
|
598
|
+
|
|
599
|
+
// Revert if no token IDs are provided.
|
|
600
|
+
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
601
|
+
|
|
602
|
+
// Only the entity authorized for these token IDs may start their vesting clock.
|
|
603
|
+
_requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
|
|
604
|
+
|
|
605
|
+
// Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
|
|
606
|
+
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// @notice Shared begin-vesting-then-collect logic across reward groups.
|
|
610
|
+
/// @param hook The hook whose stakers are collecting.
|
|
611
|
+
/// @param groupId The reward group (0 = the default group).
|
|
612
|
+
/// @param tokenIds The token IDs to collect for.
|
|
613
|
+
/// @param tokens The reward tokens to collect.
|
|
614
|
+
/// @param beneficiary The recipient of the collected tokens.
|
|
615
|
+
function _collectVestedRewards(
|
|
616
|
+
address hook,
|
|
617
|
+
uint256 groupId,
|
|
618
|
+
uint256[] calldata tokenIds,
|
|
619
|
+
IERC20[] calldata tokens,
|
|
620
|
+
address beneficiary
|
|
621
|
+
)
|
|
622
|
+
internal
|
|
623
|
+
{
|
|
624
|
+
// Collections transfer reward tokens out; block them mid inbound transfer.
|
|
625
|
+
_requireNotAcceptingToken();
|
|
626
|
+
|
|
627
|
+
// Revert if no token IDs are provided.
|
|
628
|
+
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
629
|
+
|
|
630
|
+
// Only the entity authorized for these token IDs may materialize and collect their rewards.
|
|
631
|
+
_requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
|
|
632
|
+
|
|
633
|
+
// Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
|
|
634
|
+
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
635
|
+
|
|
636
|
+
// Release whatever portion of existing vesting entries has unlocked by this round.
|
|
637
|
+
_unlockRewards({
|
|
638
|
+
hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/// @notice Shared forfeiture-release logic across reward groups.
|
|
643
|
+
/// @param hook The hook whose tokens were burned.
|
|
644
|
+
/// @param groupId The reward group (0 = the default group).
|
|
645
|
+
/// @param tokenIds The IDs of the burned tokens.
|
|
646
|
+
/// @param tokens The reward tokens to recycle.
|
|
647
|
+
/// @param beneficiary Unused for forfeiture. Kept for interface compatibility.
|
|
648
|
+
function _releaseForfeitedRewards(
|
|
649
|
+
address hook,
|
|
650
|
+
uint256 groupId,
|
|
651
|
+
uint256[] calldata tokenIds,
|
|
652
|
+
IERC20[] calldata tokens,
|
|
653
|
+
address beneficiary
|
|
654
|
+
)
|
|
655
|
+
internal
|
|
656
|
+
{
|
|
657
|
+
// Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
|
|
658
|
+
_requireNotAcceptingToken();
|
|
659
|
+
|
|
660
|
+
// Make sure that all staker token IDs are burned.
|
|
661
|
+
for (uint256 i; i < tokenIds.length;) {
|
|
662
|
+
if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
|
|
663
|
+
revert JBDistributor_NoAccess({hook: hook, tokenId: tokenIds[i], account: msg.sender});
|
|
664
|
+
}
|
|
665
|
+
unchecked {
|
|
666
|
+
++i;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Unlock the rewards and recycle the forfeited amount.
|
|
671
|
+
_unlockRewards({
|
|
672
|
+
hook: hook,
|
|
673
|
+
groupId: groupId,
|
|
674
|
+
tokenIds: tokenIds,
|
|
675
|
+
tokens: tokens,
|
|
676
|
+
beneficiary: beneficiary,
|
|
677
|
+
ownerClaim: false
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/// @notice Shared expired-reward recycling logic across reward groups.
|
|
682
|
+
/// @param hook The hook whose expired rewards should be recycled.
|
|
683
|
+
/// @param groupId The reward group (0 = the default group).
|
|
684
|
+
/// @param token The reward token to recycle.
|
|
685
|
+
/// @param rounds The reward rounds to recycle.
|
|
686
|
+
/// @return amount The total amount recycled.
|
|
687
|
+
function _burnExpiredRewards(
|
|
688
|
+
address hook,
|
|
689
|
+
uint256 groupId,
|
|
690
|
+
IERC20 token,
|
|
691
|
+
uint256[] calldata rounds
|
|
692
|
+
)
|
|
693
|
+
internal
|
|
694
|
+
returns (uint256 amount)
|
|
695
|
+
{
|
|
696
|
+
// Do not let reward-token callbacks recycle inventory during an inbound balance-delta measurement.
|
|
697
|
+
_requireNotAcceptingToken();
|
|
698
|
+
|
|
699
|
+
// Process every requested round independently so callers can batch keeper work.
|
|
700
|
+
for (uint256 i; i < rounds.length;) {
|
|
701
|
+
amount += _recycleExpiredRewardRound({hook: hook, groupId: groupId, token: token, round: rounds[i]});
|
|
702
|
+
unchecked {
|
|
703
|
+
++i;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/// @notice Shared borrow-against-vesting logic across reward groups.
|
|
709
|
+
/// @param hook The hook whose staker is borrowing against vesting rewards.
|
|
710
|
+
/// @param groupId The reward group (0 = the default group).
|
|
711
|
+
/// @param tokenIds The single token ID to borrow against.
|
|
712
|
+
/// @param tokens The single revnet reward token to collateralize.
|
|
713
|
+
/// @param sourceToken The token to borrow from the revnet.
|
|
714
|
+
/// @param minBorrowAmount The minimum amount to borrow, denominated in `sourceToken`.
|
|
715
|
+
/// @param prepaidFeePercent The fee percent to charge upfront.
|
|
716
|
+
/// @param beneficiary The recipient of the borrowed funds.
|
|
717
|
+
/// @return loanId The Revnet loan NFT ID held by this distributor.
|
|
718
|
+
/// @return collateralCount The amount of vesting rewards used as collateral.
|
|
719
|
+
function _borrowAgainstVestingFor(
|
|
720
|
+
address hook,
|
|
721
|
+
uint256 groupId,
|
|
722
|
+
uint256[] calldata tokenIds,
|
|
723
|
+
IERC20[] calldata tokens,
|
|
724
|
+
address sourceToken,
|
|
725
|
+
uint256 minBorrowAmount,
|
|
726
|
+
uint256 prepaidFeePercent,
|
|
727
|
+
address payable beneficiary
|
|
728
|
+
)
|
|
729
|
+
internal
|
|
730
|
+
returns (uint256 loanId, uint256 collateralCount)
|
|
731
|
+
{
|
|
732
|
+
// Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
|
|
733
|
+
_requireNotAcceptingToken();
|
|
734
|
+
|
|
735
|
+
// Revert if no token IDs are provided.
|
|
736
|
+
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
737
|
+
|
|
738
|
+
// One distributor-held Revnet loan tracks one token ID so one repayment restores one vesting schedule.
|
|
739
|
+
if (tokenIds.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokenIds.length});
|
|
740
|
+
|
|
741
|
+
// One loan collateralizes one revnet reward token.
|
|
742
|
+
if (tokens.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokens.length});
|
|
743
|
+
|
|
744
|
+
// Zero vesting means rewards are immediately collectible, so there is no locked position to borrow against.
|
|
745
|
+
if (VESTING_ROUNDS == 0) revert JBDistributor_VestingLoansDisabled();
|
|
746
|
+
|
|
747
|
+
// Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
|
|
748
|
+
if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
|
|
749
|
+
|
|
750
|
+
// Make sure that all tokens can be claimed by this sender.
|
|
751
|
+
_requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
|
|
752
|
+
|
|
753
|
+
// Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
|
|
754
|
+
JBBorrowContext memory ctx = JBBorrowContext({
|
|
755
|
+
hook: hook,
|
|
756
|
+
groupId: groupId,
|
|
757
|
+
tokenId: tokenIds[0],
|
|
758
|
+
token: tokens[0],
|
|
759
|
+
sourceToken: sourceToken,
|
|
760
|
+
minBorrowAmount: minBorrowAmount,
|
|
761
|
+
prepaidFeePercent: prepaidFeePercent,
|
|
762
|
+
beneficiary: beneficiary,
|
|
763
|
+
revnetId: _revnetIdOf(tokens[0])
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// Open and track the distributor-owned loan.
|
|
767
|
+
(loanId, collateralCount) = _borrowAgainstVesting({ctx: ctx, tokenIds: tokenIds, tokens: tokens});
|
|
768
|
+
}
|
|
769
|
+
|
|
667
770
|
/// @notice Open and track a distributor-held Revnet loan against one vesting position.
|
|
668
771
|
/// @param ctx The borrow context.
|
|
669
772
|
/// @param tokenIds The single token ID being collateralized.
|
|
@@ -679,7 +782,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
679
782
|
returns (uint256 loanId, uint256 collateralCount)
|
|
680
783
|
{
|
|
681
784
|
// One vesting position cannot be collateralized by two outstanding loans.
|
|
682
|
-
uint256 activeLoanId = activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token];
|
|
785
|
+
uint256 activeLoanId = activeVestingLoanIdOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token];
|
|
683
786
|
if (activeLoanId != 0) {
|
|
684
787
|
revert JBDistributor_VestingLoanOutstanding({
|
|
685
788
|
hook: ctx.hook, tokenId: ctx.tokenId, token: address(ctx.token), loanId: activeLoanId
|
|
@@ -687,13 +790,14 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
687
790
|
}
|
|
688
791
|
|
|
689
792
|
// Bring the claimant current before measuring collateral.
|
|
690
|
-
_claimPastRewards({hook: ctx.hook, tokenIds: tokenIds, tokens: tokens});
|
|
793
|
+
_claimPastRewards({hook: ctx.hook, groupId: ctx.groupId, tokenIds: tokenIds, tokens: tokens});
|
|
691
794
|
|
|
692
795
|
// Use the remaining uncollected vesting amount as collateral without advancing the vesting schedule.
|
|
693
|
-
collateralCount =
|
|
796
|
+
collateralCount =
|
|
797
|
+
_unclaimedVestingAmountOf({hook: ctx.hook, groupId: ctx.groupId, tokenId: ctx.tokenId, token: ctx.token});
|
|
694
798
|
|
|
695
799
|
// 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);
|
|
800
|
+
uint48 vestingDataCount = _toUint48(vestingDataOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token].length);
|
|
697
801
|
|
|
698
802
|
// A zero-collateral loan would revert in Revnet, but this local error explains why.
|
|
699
803
|
if (collateralCount == 0) {
|
|
@@ -706,7 +810,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
706
810
|
totalLoanedVestingAmountOf[ctx.hook][ctx.token] += collateralCount;
|
|
707
811
|
|
|
708
812
|
// 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;
|
|
813
|
+
activeVestingLoanIdOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token] = _PENDING_VESTING_LOAN_ID;
|
|
710
814
|
|
|
711
815
|
// Open the Revnet loan with this distributor as the holder whose tokens are burned as collateral.
|
|
712
816
|
loanId = _openVestingLoan({ctx: ctx, collateralCount: collateralCount});
|
|
@@ -715,9 +819,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
715
819
|
}
|
|
716
820
|
|
|
717
821
|
// Track the distributor-held loan so repayment can restore the same vesting position.
|
|
718
|
-
activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token] = loanId;
|
|
822
|
+
activeVestingLoanIdOf[ctx.hook][ctx.groupId][ctx.tokenId][ctx.token] = loanId;
|
|
719
823
|
_vestingLoanOf[loanId] = JBVestingLoan({
|
|
720
824
|
hook: ctx.hook,
|
|
825
|
+
groupId: ctx.groupId,
|
|
721
826
|
tokenId: ctx.tokenId,
|
|
722
827
|
token: ctx.token,
|
|
723
828
|
vestingDataCount: vestingDataCount,
|
|
@@ -764,11 +869,14 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
764
869
|
}
|
|
765
870
|
|
|
766
871
|
/// @notice Repay a Revnet loan with the source token it borrowed.
|
|
872
|
+
/// @dev Any native overpayment is reported via `nativeRefundAmount` instead of being refunded here, so the caller
|
|
873
|
+
/// can settle the loan's state before returning the overpayment (checks-effects-interactions).
|
|
767
874
|
/// @param loanId The Revnet loan NFT ID to repay.
|
|
768
875
|
/// @param loan The Revnet loan data.
|
|
769
876
|
/// @param repayBorrowAmount The amount of source token needed to repay the loan.
|
|
770
877
|
/// @param collateralCount The amount of collateral to return.
|
|
771
878
|
/// @return paidOffLoanId The paid-off loan ID returned by Revnet loans.
|
|
879
|
+
/// @return nativeRefundAmount The native overpayment the caller must refund after settling the loan.
|
|
772
880
|
function _repayLoanSource(
|
|
773
881
|
uint256 loanId,
|
|
774
882
|
REVLoan memory loan,
|
|
@@ -776,7 +884,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
776
884
|
uint256 collateralCount
|
|
777
885
|
)
|
|
778
886
|
internal
|
|
779
|
-
returns (uint256 paidOffLoanId)
|
|
887
|
+
returns (uint256 paidOffLoanId, uint256 nativeRefundAmount)
|
|
780
888
|
{
|
|
781
889
|
JBSingleAllowance memory allowance;
|
|
782
890
|
|
|
@@ -795,14 +903,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
795
903
|
allowance: allowance
|
|
796
904
|
});
|
|
797
905
|
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
if (refundAmount != 0) {
|
|
801
|
-
(bool success,) = msg.sender.call{value: refundAmount}("");
|
|
802
|
-
if (!success) {
|
|
803
|
-
revert JBDistributor_NativeTransferFailed({beneficiary: msg.sender, amount: refundAmount});
|
|
804
|
-
}
|
|
805
|
-
}
|
|
906
|
+
// Report any native overpayment so the caller can refund it only after the loan's state is settled.
|
|
907
|
+
nativeRefundAmount = msg.value - repayBorrowAmount;
|
|
806
908
|
} else {
|
|
807
909
|
// ERC-20 repayments must not carry native ETH.
|
|
808
910
|
if (msg.value != 0) {
|
|
@@ -867,7 +969,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
867
969
|
totalLoanedVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= vestingLoan.collateralCount;
|
|
868
970
|
|
|
869
971
|
// Clear the lock that prevented this position from being collected while collateralized.
|
|
870
|
-
delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
|
|
972
|
+
delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
|
|
871
973
|
delete _vestingLoanOf[loanId];
|
|
872
974
|
|
|
873
975
|
// Return any excess reward tokens created during source-fee payment to the repayer.
|
|
@@ -901,10 +1003,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
901
1003
|
collateralCount = vestingLoan.collateralCount;
|
|
902
1004
|
|
|
903
1005
|
// Load the vesting entries for the token ID whose rewards were collateralized.
|
|
904
|
-
JBVestingData[] storage vestings =
|
|
1006
|
+
JBVestingData[] storage vestings =
|
|
1007
|
+
vestingDataOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
|
|
905
1008
|
|
|
906
1009
|
// Start at the first unexhausted vesting entry.
|
|
907
|
-
uint256 vestedIndex =
|
|
1010
|
+
uint256 vestedIndex =
|
|
1011
|
+
latestVestedIndexOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
|
|
908
1012
|
|
|
909
1013
|
// Stop at the boundary recorded when the loan opened, preserving newer vesting entries.
|
|
910
1014
|
uint256 vestingDataCount = vestingLoan.vestingDataCount;
|
|
@@ -920,7 +1024,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
920
1024
|
}
|
|
921
1025
|
|
|
922
1026
|
// Skip over the written-off vesting entries without ever moving the cursor backwards.
|
|
923
|
-
latestVestedIndexOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token] = vestedIndex;
|
|
1027
|
+
latestVestedIndexOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token] = vestedIndex;
|
|
924
1028
|
|
|
925
1029
|
// Remove the liquidated collateral from the amount still considered vesting.
|
|
926
1030
|
totalVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= collateralCount;
|
|
@@ -929,7 +1033,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
929
1033
|
totalLoanedVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= collateralCount;
|
|
930
1034
|
|
|
931
1035
|
// Clear the active loan lock for this token ID and reward token.
|
|
932
|
-
delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
|
|
1036
|
+
delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.groupId][vestingLoan.tokenId][vestingLoan.token];
|
|
933
1037
|
|
|
934
1038
|
// Clear the loan metadata so it cannot be written off or repaid again.
|
|
935
1039
|
delete _vestingLoanOf[loanId];
|
|
@@ -980,9 +1084,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
980
1084
|
|
|
981
1085
|
/// @notice Accept funds and assign them to this round's reward ledger.
|
|
982
1086
|
/// @param hook The stake source whose stakers receive the rewards.
|
|
1087
|
+
/// @param groupId The reward group being funded (0 = the default group).
|
|
983
1088
|
/// @param token The reward token being funded.
|
|
984
1089
|
/// @param amount The nominal amount to fund.
|
|
985
|
-
function _fund(address hook, IERC20 token, uint256 amount) internal {
|
|
1090
|
+
function _fund(address hook, uint256 groupId, IERC20 token, uint256 amount) internal {
|
|
986
1091
|
// Native funding is measured by msg.value, not the caller-provided amount.
|
|
987
1092
|
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
988
1093
|
amount = msg.value;
|
|
@@ -997,38 +1102,41 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
997
1102
|
}
|
|
998
1103
|
|
|
999
1104
|
// Store the accepted amount in this round's historical reward ledger.
|
|
1000
|
-
_recordRewardFunding({hook: hook, token: token, amount: amount});
|
|
1105
|
+
_recordRewardFunding({hook: hook, groupId: groupId, token: token, amount: amount});
|
|
1001
1106
|
}
|
|
1002
1107
|
|
|
1003
1108
|
/// @notice Record accepted funding as the current round's reward pot.
|
|
1004
1109
|
/// @param hook The stake source whose stakers receive the rewards.
|
|
1110
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1005
1111
|
/// @param token The reward token.
|
|
1006
1112
|
/// @param amount The accepted funding amount.
|
|
1007
|
-
function _recordRewardFunding(address hook, IERC20 token, uint256 amount) internal {
|
|
1113
|
+
function _recordRewardFunding(address hook, uint256 groupId, IERC20 token, uint256 amount) internal {
|
|
1008
1114
|
// Zero-value transfers do not create reward rounds or alter tracked balances.
|
|
1009
1115
|
if (amount == 0) return;
|
|
1010
1116
|
|
|
1011
1117
|
// Add the accepted amount to the current reward ledger.
|
|
1012
|
-
_recordRewardRound({hook: hook, token: token, amount: amount});
|
|
1118
|
+
_recordRewardRound({hook: hook, groupId: groupId, token: token, amount: amount});
|
|
1013
1119
|
|
|
1014
|
-
// Keep the base distributor's balance accounting in sync for collection and conservation checks.
|
|
1120
|
+
// Keep the base distributor's balance accounting in sync for collection and conservation checks. Balances
|
|
1121
|
+
// are tracked per (hook, token) across all groups because they share one token custody pool.
|
|
1015
1122
|
_balanceOf[hook][token] += amount;
|
|
1016
1123
|
_accountedBalanceOf[token] += amount;
|
|
1017
1124
|
}
|
|
1018
1125
|
|
|
1019
1126
|
/// @notice Record rewards as the current round's claimable historical reward pot.
|
|
1020
1127
|
/// @param hook The stake source whose stakers receive the rewards.
|
|
1128
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1021
1129
|
/// @param token The reward token.
|
|
1022
1130
|
/// @param amount The amount to add to the current reward round.
|
|
1023
|
-
function _recordRewardRound(address hook, IERC20 token, uint256 amount) internal {
|
|
1131
|
+
function _recordRewardRound(address hook, uint256 groupId, IERC20 token, uint256 amount) internal {
|
|
1024
1132
|
// Zero-value rewards do not create reward rounds.
|
|
1025
1133
|
if (amount == 0) return;
|
|
1026
1134
|
|
|
1027
1135
|
// Rewards belong to the round in progress when they enter the ledger.
|
|
1028
1136
|
uint256 round = currentRound();
|
|
1029
1137
|
|
|
1030
|
-
// Load the current round's ledger entry for this hook and reward token.
|
|
1031
|
-
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
|
|
1138
|
+
// Load the current round's ledger entry for this hook, group, and reward token.
|
|
1139
|
+
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][groupId][token][round];
|
|
1032
1140
|
|
|
1033
1141
|
// Every reward round in this contract uses the same immutable claim duration.
|
|
1034
1142
|
uint48 claimDeadline = _claimDeadlineFor(round);
|
|
@@ -1044,8 +1152,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1044
1152
|
// Store the packed claim deadline fixed for this distributor.
|
|
1045
1153
|
rewardRound.claimDeadline = claimDeadline;
|
|
1046
1154
|
|
|
1047
|
-
// Store the packed total stake that shares this
|
|
1048
|
-
rewardRound.totalStake = _toUint208(_totalStake({hook: hook, blockNumber: snapshotBlock}));
|
|
1155
|
+
// Store the packed total stake that shares this group's round reward pot.
|
|
1156
|
+
rewardRound.totalStake = _toUint208(_totalStake({hook: hook, groupId: groupId, blockNumber: snapshotBlock}));
|
|
1049
1157
|
}
|
|
1050
1158
|
|
|
1051
1159
|
// Multiple additions in the same round share the same snapshot and reward pot.
|
|
@@ -1054,11 +1162,13 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1054
1162
|
|
|
1055
1163
|
/// @notice Recycle one expired reward round's unclaimed inventory into the current reward round.
|
|
1056
1164
|
/// @param hook The hook whose expired rewards should be recycled.
|
|
1165
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1057
1166
|
/// @param token The reward token to recycle.
|
|
1058
1167
|
/// @param round The reward round to recycle.
|
|
1059
1168
|
/// @return recycleAmount The amount recycled.
|
|
1060
1169
|
function _recycleExpiredRewardRound(
|
|
1061
1170
|
address hook,
|
|
1171
|
+
uint256 groupId,
|
|
1062
1172
|
IERC20 token,
|
|
1063
1173
|
uint256 round
|
|
1064
1174
|
)
|
|
@@ -1066,7 +1176,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1066
1176
|
returns (uint256 recycleAmount)
|
|
1067
1177
|
{
|
|
1068
1178
|
// Load the reward round once so expiry, claimed amount, and funded amount stay in sync.
|
|
1069
|
-
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
|
|
1179
|
+
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][groupId][token][round];
|
|
1070
1180
|
|
|
1071
1181
|
// Ignore rounds that either never expire or have not reached their deadline yet.
|
|
1072
1182
|
if (!_rewardRoundExpired(rewardRound)) return 0;
|
|
@@ -1082,7 +1192,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1082
1192
|
|
|
1083
1193
|
// Keep the inventory in the distributor and give the current staker set a new claimable round.
|
|
1084
1194
|
uint256 recycledToRound = currentRound();
|
|
1085
|
-
_recordRewardRound({hook: hook, token: token, amount: recycleAmount});
|
|
1195
|
+
_recordRewardRound({hook: hook, groupId: groupId, token: token, amount: recycleAmount});
|
|
1086
1196
|
|
|
1087
1197
|
// Surface the permissionless recycle for off-chain accounting.
|
|
1088
1198
|
emit ExpiredRewardsRecycled({
|
|
@@ -1174,12 +1284,14 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1174
1284
|
|
|
1175
1285
|
/// @notice Unlocks rewards for the given token IDs and tokens, either for collection or forfeiture.
|
|
1176
1286
|
/// @param hook The hook the tokens belong to.
|
|
1287
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1177
1288
|
/// @param tokenIds The IDs of the tokens to unlock rewards for.
|
|
1178
1289
|
/// @param tokens The addresses of the tokens to unlock.
|
|
1179
1290
|
/// @param beneficiary The recipient of the unlocked tokens.
|
|
1180
1291
|
/// @param ownerClaim Whether this is a claim by the owner (true) or a forfeiture release (false).
|
|
1181
1292
|
function _unlockRewards(
|
|
1182
1293
|
address hook,
|
|
1294
|
+
uint256 groupId,
|
|
1183
1295
|
uint256[] calldata tokenIds,
|
|
1184
1296
|
IERC20[] calldata tokens,
|
|
1185
1297
|
address beneficiary,
|
|
@@ -1194,7 +1306,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1194
1306
|
IERC20 token = tokens[i];
|
|
1195
1307
|
|
|
1196
1308
|
// Process all token IDs for this reward token.
|
|
1197
|
-
uint256 totalTokenAmount =
|
|
1309
|
+
uint256 totalTokenAmount =
|
|
1310
|
+
_unlockTokenIds({hook: hook, groupId: groupId, tokenIds: tokenIds, token: token, round: round});
|
|
1198
1311
|
|
|
1199
1312
|
// Perform the transfer.
|
|
1200
1313
|
if (totalTokenAmount != 0) {
|
|
@@ -1221,7 +1334,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1221
1334
|
}
|
|
1222
1335
|
} else {
|
|
1223
1336
|
// If forfeiture: keep inventory in the distributor and give the current staker set a fresh round.
|
|
1224
|
-
_recordRewardRound({hook: hook, token: token, amount: totalTokenAmount});
|
|
1337
|
+
_recordRewardRound({hook: hook, groupId: groupId, token: token, amount: totalTokenAmount});
|
|
1225
1338
|
emit ForfeitedRewardsRecycled({
|
|
1226
1339
|
hook: hook, round: round, token: token, amount: totalTokenAmount, caller: msg.sender
|
|
1227
1340
|
});
|
|
@@ -1236,12 +1349,14 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1236
1349
|
|
|
1237
1350
|
/// @notice Unlocks rewards for a set of token IDs for a single reward token.
|
|
1238
1351
|
/// @param hook The hook the tokens belong to.
|
|
1352
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1239
1353
|
/// @param tokenIds The IDs of the tokens to unlock rewards for.
|
|
1240
1354
|
/// @param token The reward token to unlock.
|
|
1241
1355
|
/// @param round The current round.
|
|
1242
1356
|
/// @return totalTokenAmount The total amount of reward tokens unlocked.
|
|
1243
1357
|
function _unlockTokenIds(
|
|
1244
1358
|
address hook,
|
|
1359
|
+
uint256 groupId,
|
|
1245
1360
|
uint256[] calldata tokenIds,
|
|
1246
1361
|
IERC20 token,
|
|
1247
1362
|
uint256 round
|
|
@@ -1253,13 +1368,13 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1253
1368
|
uint256 tokenId = tokenIds[j];
|
|
1254
1369
|
|
|
1255
1370
|
// Loan collateral stays locked until repayment restores it to this distributor.
|
|
1256
|
-
_requireNoActiveVestingLoan({hook: hook, tokenId: tokenId, token: token});
|
|
1371
|
+
_requireNoActiveVestingLoan({hook: hook, groupId: groupId, tokenId: tokenId, token: token});
|
|
1257
1372
|
|
|
1258
1373
|
// Keep a reference to the latest vested index.
|
|
1259
|
-
uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
|
|
1374
|
+
uint256 vestedIndex = latestVestedIndexOf[hook][groupId][tokenId][token];
|
|
1260
1375
|
|
|
1261
1376
|
// Keep a reference to the vesting data array.
|
|
1262
|
-
JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
|
|
1377
|
+
JBVestingData[] storage vestings = vestingDataOf[hook][groupId][tokenId][token];
|
|
1263
1378
|
uint256 numberOfVestingRounds = vestings.length;
|
|
1264
1379
|
|
|
1265
1380
|
// Keep a reference to a vested index that will be incremented.
|
|
@@ -1294,6 +1409,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1294
1409
|
emit Collected({
|
|
1295
1410
|
hook: hook,
|
|
1296
1411
|
tokenId: tokenId,
|
|
1412
|
+
groupId: groupId,
|
|
1297
1413
|
token: token,
|
|
1298
1414
|
amount: claimAmount,
|
|
1299
1415
|
vestingReleaseRound: vesting.releaseRound,
|
|
@@ -1315,7 +1431,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1315
1431
|
}
|
|
1316
1432
|
}
|
|
1317
1433
|
|
|
1318
|
-
latestVestedIndexOf[hook][tokenId][token] = newLatestVestedIndex;
|
|
1434
|
+
latestVestedIndexOf[hook][groupId][tokenId][token] = newLatestVestedIndex;
|
|
1319
1435
|
|
|
1320
1436
|
unchecked {
|
|
1321
1437
|
++j;
|
|
@@ -1327,13 +1443,73 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1327
1443
|
// ----------------------- internal views ---------------------------- //
|
|
1328
1444
|
//*********************************************************************//
|
|
1329
1445
|
|
|
1446
|
+
/// @notice The collectable (unlocked, uncollected) amount for a token ID in a specific reward group.
|
|
1447
|
+
/// @param hook The hook the tokenId belongs to.
|
|
1448
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1449
|
+
/// @param tokenId The ID of the staker token to calculate for.
|
|
1450
|
+
/// @param token The reward token to check.
|
|
1451
|
+
/// @return tokenAmount The amount of tokens that can be collected right now.
|
|
1452
|
+
function _collectableFor(
|
|
1453
|
+
address hook,
|
|
1454
|
+
uint256 groupId,
|
|
1455
|
+
uint256 tokenId,
|
|
1456
|
+
IERC20 token
|
|
1457
|
+
)
|
|
1458
|
+
internal
|
|
1459
|
+
view
|
|
1460
|
+
returns (uint256 tokenAmount)
|
|
1461
|
+
{
|
|
1462
|
+
// A loan keeps this token ID's vesting rewards in collateral custody until the loan is repaid.
|
|
1463
|
+
if (activeVestingLoanIdOf[hook][groupId][tokenId][token] != 0) return 0;
|
|
1464
|
+
|
|
1465
|
+
// The round that we are in right now.
|
|
1466
|
+
uint256 round = currentRound();
|
|
1467
|
+
|
|
1468
|
+
// Keep a reference to the latest vested index.
|
|
1469
|
+
uint256 vestedIndex = latestVestedIndexOf[hook][groupId][tokenId][token];
|
|
1470
|
+
|
|
1471
|
+
// Keep a reference to the vesting data array.
|
|
1472
|
+
JBVestingData[] storage vestings = vestingDataOf[hook][groupId][tokenId][token];
|
|
1473
|
+
uint256 numberOfVestingRounds = vestings.length;
|
|
1474
|
+
|
|
1475
|
+
while (vestedIndex < numberOfVestingRounds) {
|
|
1476
|
+
uint256 lockedShare;
|
|
1477
|
+
|
|
1478
|
+
// Keep a reference to the vested data being iterated on.
|
|
1479
|
+
JBVestingData memory vesting = vestings[vestedIndex];
|
|
1480
|
+
|
|
1481
|
+
lockedShare = JBVestingMath.lockedShareOf({
|
|
1482
|
+
releaseRound: vesting.releaseRound,
|
|
1483
|
+
currentRound: round,
|
|
1484
|
+
vestingRounds: VESTING_ROUNDS,
|
|
1485
|
+
maxShare: MAX_SHARE
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
// Calculate the newly unlocked amount from cumulative shares rather than the incremental share delta.
|
|
1489
|
+
// Incremental floor rounding can otherwise underpay partial collections and leave dust stranded.
|
|
1490
|
+
(uint256 claimAmount,) = JBVestingMath.newlyClaimableAmountOf({
|
|
1491
|
+
amount: vesting.amount,
|
|
1492
|
+
shareClaimed: vesting.shareClaimed,
|
|
1493
|
+
lockedShare: lockedShare,
|
|
1494
|
+
maxShare: MAX_SHARE
|
|
1495
|
+
});
|
|
1496
|
+
tokenAmount += claimAmount;
|
|
1497
|
+
|
|
1498
|
+
unchecked {
|
|
1499
|
+
++vestedIndex;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1330
1504
|
/// @notice The remaining uncollected vesting amount for one token ID and reward token.
|
|
1331
1505
|
/// @param hook The hook the token ID belongs to.
|
|
1506
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1332
1507
|
/// @param tokenId The token ID to check.
|
|
1333
1508
|
/// @param token The reward token to check.
|
|
1334
1509
|
/// @return tokenAmount The amount still locked or unlocked-but-uncollected.
|
|
1335
1510
|
function _unclaimedVestingAmountOf(
|
|
1336
1511
|
address hook,
|
|
1512
|
+
uint256 groupId,
|
|
1337
1513
|
uint256 tokenId,
|
|
1338
1514
|
IERC20 token
|
|
1339
1515
|
)
|
|
@@ -1342,10 +1518,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1342
1518
|
returns (uint256 tokenAmount)
|
|
1343
1519
|
{
|
|
1344
1520
|
// Keep a reference to the latest fully vested index.
|
|
1345
|
-
uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
|
|
1521
|
+
uint256 vestedIndex = latestVestedIndexOf[hook][groupId][tokenId][token];
|
|
1346
1522
|
|
|
1347
1523
|
// Keep a reference to the vesting data array.
|
|
1348
|
-
JBVestingData[] storage vestings = vestingDataOf[hook][tokenId][token];
|
|
1524
|
+
JBVestingData[] storage vestings = vestingDataOf[hook][groupId][tokenId][token];
|
|
1349
1525
|
uint256 numberOfVestingRounds = vestings.length;
|
|
1350
1526
|
|
|
1351
1527
|
while (vestedIndex < numberOfVestingRounds) {
|
|
@@ -1381,10 +1557,11 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1381
1557
|
|
|
1382
1558
|
/// @notice Revert if a token ID's vesting rewards are locked in a distributor-owned loan.
|
|
1383
1559
|
/// @param hook The hook the token ID belongs to.
|
|
1560
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1384
1561
|
/// @param tokenId The token ID to check.
|
|
1385
1562
|
/// @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];
|
|
1563
|
+
function _requireNoActiveVestingLoan(address hook, uint256 groupId, uint256 tokenId, IERC20 token) internal view {
|
|
1564
|
+
uint256 loanId = activeVestingLoanIdOf[hook][groupId][tokenId][token];
|
|
1388
1565
|
if (loanId != 0) {
|
|
1389
1566
|
revert JBDistributor_VestingLoanOutstanding({
|
|
1390
1567
|
hook: hook, tokenId: tokenId, token: address(token), loanId: loanId
|
|
@@ -1400,17 +1577,26 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1400
1577
|
function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
|
|
1401
1578
|
|
|
1402
1579
|
/// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
|
|
1403
|
-
///
|
|
1580
|
+
/// @dev Subclasses define how stake is measured.
|
|
1404
1581
|
/// @param hook The hook the token belongs to.
|
|
1405
1582
|
/// @param tokenId The ID of the token to get the stake weight of.
|
|
1406
1583
|
/// @return tokenStakeAmount The stake weight represented by this token ID.
|
|
1407
1584
|
function _tokenStake(address hook, uint256 tokenId) internal view virtual returns (uint256 tokenStakeAmount);
|
|
1408
1585
|
|
|
1409
|
-
/// @notice The total stake
|
|
1410
|
-
/// token ID's pro-rata share.
|
|
1411
|
-
///
|
|
1586
|
+
/// @notice The total stake sharing a group's round rewards at a given block. Used as the denominator when
|
|
1587
|
+
/// calculating each token ID's pro-rata share.
|
|
1588
|
+
/// @dev Subclasses define how the per-group total stake is measured.
|
|
1412
1589
|
/// @param hook The hook to get the total stake for.
|
|
1590
|
+
/// @param groupId The reward group (0 = the default group).
|
|
1413
1591
|
/// @param blockNumber The block number to query (must be strictly in the past).
|
|
1414
1592
|
/// @return totalStakedAmount The total stake at the given block.
|
|
1415
|
-
function _totalStake(
|
|
1593
|
+
function _totalStake(
|
|
1594
|
+
address hook,
|
|
1595
|
+
uint256 groupId,
|
|
1596
|
+
uint256 blockNumber
|
|
1597
|
+
)
|
|
1598
|
+
internal
|
|
1599
|
+
view
|
|
1600
|
+
virtual
|
|
1601
|
+
returns (uint256 totalStakedAmount);
|
|
1416
1602
|
}
|