@bananapus/distributor-v6 0.0.38 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -21
- package/package.json +6 -6
- package/references/operations.md +3 -3
- package/references/runtime.md +1 -1
- package/src/JB721Distributor.sol +198 -166
- package/src/JBDistributor.sol +110 -51
- package/src/JBTokenDistributor.sol +135 -82
- package/src/interfaces/IJB721Distributor.sol +27 -21
- package/src/interfaces/IJBDistributor.sol +60 -34
- package/src/interfaces/IJBTokenDistributor.sol +4 -3
- package/src/libraries/JBVestingMath.sol +1 -1
- package/src/structs/JBRewardRoundData.sol +2 -2
package/src/JBDistributor.sol
CHANGED
|
@@ -27,9 +27,10 @@ import {JBVestingLoan} from "./structs/JBVestingLoan.sol";
|
|
|
27
27
|
/// to stakers with linear vesting. Each round, a snapshot is taken of the distributable balance, and stakers can
|
|
28
28
|
/// claim their pro-rata share based on their stake weight at the snapshot block. Claimed tokens vest linearly over
|
|
29
29
|
/// `VESTING_ROUNDS` rounds and can be collected as they unlock.
|
|
30
|
-
/// @dev Subclasses define how stake is measured (`_tokenStake`, `_totalStake`), who can
|
|
31
|
-
///
|
|
32
|
-
///
|
|
30
|
+
/// @dev Subclasses define how stake is measured (`_tokenStake`, `_totalStake`), who can redirect collected rewards
|
|
31
|
+
/// (`_claimBeneficiaryOf`, `_canClaim`), how token IDs are validated (`_validateTokenIds`), and what "burned" means
|
|
32
|
+
/// (`_tokenBurned`). Two concrete implementations exist: `JBTokenDistributor` (IVotes tokens) and `JB721Distributor`
|
|
33
|
+
/// (Juicebox 721 NFTs).
|
|
33
34
|
abstract contract JBDistributor is IJBDistributor {
|
|
34
35
|
using SafeERC20 for IERC20;
|
|
35
36
|
|
|
@@ -222,6 +223,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
222
223
|
// -------------------------- constructor ---------------------------- //
|
|
223
224
|
//*********************************************************************//
|
|
224
225
|
|
|
226
|
+
/// @notice Initializes the shared distributor configuration.
|
|
225
227
|
/// @param controller The JB controller used for token registry lookups and revnet loan permissions.
|
|
226
228
|
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
227
229
|
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
@@ -266,10 +268,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
266
268
|
//*********************************************************************//
|
|
267
269
|
|
|
268
270
|
/// @notice Begin vesting all unclaimed past reward rounds for the specified token IDs.
|
|
269
|
-
/// @dev Materializes each token ID's pro-rata share of every past
|
|
271
|
+
/// @dev Permissionless. Materializes each token ID's pro-rata share of every past reward round into fresh vesting
|
|
270
272
|
/// entries that start now and unlock over `VESTING_ROUNDS`. Current-round funding is excluded until a later round
|
|
271
|
-
/// starts. The model-specific per-round claim math and
|
|
272
|
-
///
|
|
273
|
+
/// starts. The model-specific per-round claim math and token ID validation live in the `_claimPastRewards` and
|
|
274
|
+
/// `_validateTokenIds` hooks each concrete distributor implements.
|
|
273
275
|
/// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
|
|
274
276
|
/// @param tokenIds The staker token IDs to claim rewards for.
|
|
275
277
|
/// @param tokens The reward tokens to begin vesting.
|
|
@@ -296,13 +298,19 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
296
298
|
_fund({hook: hook, groupId: 0, token: token, amount: amount});
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
/// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
|
|
302
|
+
/// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
|
|
303
|
+
function poke() external override {
|
|
304
|
+
_ensureSnapshotBlock(currentRound());
|
|
305
|
+
}
|
|
306
|
+
|
|
299
307
|
/// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
|
|
300
308
|
/// @dev Recycling is permissionless; any keeper or frontend can sweep an expired round.
|
|
301
309
|
/// @param hook The hook whose expired rewards should be recycled.
|
|
302
310
|
/// @param token The reward token to recycle.
|
|
303
311
|
/// @param rounds The reward rounds to recycle.
|
|
304
312
|
/// @return amount The total amount recycled.
|
|
305
|
-
function
|
|
313
|
+
function recycleExpiredRewards(
|
|
306
314
|
address hook,
|
|
307
315
|
IERC20 token,
|
|
308
316
|
uint256[] calldata rounds
|
|
@@ -312,17 +320,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
312
320
|
override
|
|
313
321
|
returns (uint256 amount)
|
|
314
322
|
{
|
|
315
|
-
amount =
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
|
|
319
|
-
/// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
|
|
320
|
-
function poke() external override {
|
|
321
|
-
_ensureSnapshotBlock(currentRound());
|
|
323
|
+
amount = _recycleExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
|
|
322
324
|
}
|
|
323
325
|
|
|
324
|
-
/// @notice Recycle
|
|
325
|
-
/// @dev Anyone can call this for burned tokens.
|
|
326
|
+
/// @notice Recycle rewards tied to burned tokens into the current reward round as they unlock.
|
|
327
|
+
/// @dev Anyone can call this for burned tokens. Unclaimed historical shares are materialized before unlocked
|
|
328
|
+
/// forfeited amounts are recycled.
|
|
326
329
|
/// @param hook The hook whose tokens were burned.
|
|
327
330
|
/// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
|
|
328
331
|
/// @param tokens The reward tokens to recycle.
|
|
@@ -413,27 +416,6 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
413
416
|
// ----------------------- public transactions ----------------------- //
|
|
414
417
|
//*********************************************************************//
|
|
415
418
|
|
|
416
|
-
/// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
|
|
417
|
-
/// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
|
|
418
|
-
/// @dev The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
|
|
419
|
-
/// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
|
|
420
|
-
/// @param hook The hook whose stakers are collecting.
|
|
421
|
-
/// @param tokenIds The IDs of the tokens to collect for (caller must be authorized for all of them).
|
|
422
|
-
/// @param tokens The reward tokens to collect vested amounts of.
|
|
423
|
-
/// @param beneficiary The recipient of the collected tokens.
|
|
424
|
-
function collectVestedRewards(
|
|
425
|
-
address hook,
|
|
426
|
-
uint256[] calldata tokenIds,
|
|
427
|
-
IERC20[] calldata tokens,
|
|
428
|
-
address beneficiary
|
|
429
|
-
)
|
|
430
|
-
public
|
|
431
|
-
virtual
|
|
432
|
-
override
|
|
433
|
-
{
|
|
434
|
-
_collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
419
|
/// @notice Borrow from a revnet using one token ID's uncollected vesting rewards as collateral.
|
|
438
420
|
/// @dev The distributor keeps custody of the loan NFT. Collection is blocked until repayment restores the
|
|
439
421
|
/// collateral to the original vesting schedule.
|
|
@@ -472,6 +454,27 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
472
454
|
});
|
|
473
455
|
}
|
|
474
456
|
|
|
457
|
+
/// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
|
|
458
|
+
/// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
|
|
459
|
+
/// @dev Authorized holders can collect to any beneficiary. Helpers can collect only to the canonical beneficiary
|
|
460
|
+
/// for every token ID they do not control.
|
|
461
|
+
/// @param hook The hook whose stakers are collecting.
|
|
462
|
+
/// @param tokenIds The IDs of the tokens to collect for.
|
|
463
|
+
/// @param tokens The reward tokens to collect vested amounts of.
|
|
464
|
+
/// @param beneficiary The recipient of the collected tokens.
|
|
465
|
+
function collectVestedRewards(
|
|
466
|
+
address hook,
|
|
467
|
+
uint256[] calldata tokenIds,
|
|
468
|
+
IERC20[] calldata tokens,
|
|
469
|
+
address beneficiary
|
|
470
|
+
)
|
|
471
|
+
public
|
|
472
|
+
virtual
|
|
473
|
+
override
|
|
474
|
+
{
|
|
475
|
+
_collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
|
|
476
|
+
}
|
|
477
|
+
|
|
475
478
|
/// @notice Repay a distributor-held Revnet loan and restore its collateral to the original vesting schedule.
|
|
476
479
|
/// @param loanId The Revnet loan NFT ID to repay.
|
|
477
480
|
/// @param maxRepayBorrowAmount The maximum source-token amount the caller is willing to repay.
|
|
@@ -576,11 +579,6 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
576
579
|
internal
|
|
577
580
|
virtual;
|
|
578
581
|
|
|
579
|
-
/// @notice Revert unless the caller is authorized to claim each token ID.
|
|
580
|
-
/// @param hook The hook whose token IDs are being checked.
|
|
581
|
-
/// @param tokenIds The token IDs to check.
|
|
582
|
-
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
|
|
583
|
-
|
|
584
582
|
/// @notice Shared begin-vesting logic across reward groups.
|
|
585
583
|
/// @param hook The hook whose stakers are vesting.
|
|
586
584
|
/// @param groupId The reward group (0 = the default group).
|
|
@@ -600,8 +598,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
600
598
|
// Revert if no token IDs are provided.
|
|
601
599
|
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
602
600
|
|
|
603
|
-
//
|
|
604
|
-
|
|
601
|
+
// Validate token IDs before a permissionless helper can materialize vesting state.
|
|
602
|
+
_validateTokenIds({hook: hook, tokenIds: tokenIds});
|
|
605
603
|
|
|
606
604
|
// Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
|
|
607
605
|
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
@@ -628,8 +626,11 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
628
626
|
// Revert if no token IDs are provided.
|
|
629
627
|
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
630
628
|
|
|
631
|
-
//
|
|
632
|
-
|
|
629
|
+
// Validate token IDs before a permissionless helper can materialize vesting state.
|
|
630
|
+
_validateTokenIds({hook: hook, tokenIds: tokenIds});
|
|
631
|
+
|
|
632
|
+
// Only authorized holders can redirect rewards; helpers must send them to the canonical beneficiary.
|
|
633
|
+
_requireCanCollectTo({hook: hook, tokenIds: tokenIds, beneficiary: beneficiary});
|
|
633
634
|
|
|
634
635
|
// Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
|
|
635
636
|
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
@@ -641,6 +642,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
641
642
|
}
|
|
642
643
|
|
|
643
644
|
/// @notice Shared forfeiture-release logic across reward groups.
|
|
645
|
+
/// @dev Materializes unclaimed historical shares for burned token IDs before recycling the currently unlocked
|
|
646
|
+
/// forfeited amount.
|
|
644
647
|
/// @param hook The hook whose tokens were burned.
|
|
645
648
|
/// @param groupId The reward group (0 = the default group).
|
|
646
649
|
/// @param tokenIds The IDs of the burned tokens.
|
|
@@ -658,6 +661,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
658
661
|
// Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
|
|
659
662
|
_requireNotAcceptingToken();
|
|
660
663
|
|
|
664
|
+
// Let concrete distributors enforce forfeiture-only validation before claim cursors can move.
|
|
665
|
+
_validateForfeitedTokenIds({hook: hook, tokenIds: tokenIds});
|
|
666
|
+
|
|
661
667
|
// Make sure that all staker token IDs are burned.
|
|
662
668
|
for (uint256 i; i < tokenIds.length;) {
|
|
663
669
|
if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
|
|
@@ -668,7 +674,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
668
674
|
}
|
|
669
675
|
}
|
|
670
676
|
|
|
671
|
-
//
|
|
677
|
+
// Materialize any still-unclaimed historical shares using the same reward math as live claims.
|
|
678
|
+
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
679
|
+
|
|
680
|
+
// Unlock the vested forfeiture amount and recycle it into the current reward round.
|
|
672
681
|
_unlockRewards({
|
|
673
682
|
hook: hook,
|
|
674
683
|
groupId: groupId,
|
|
@@ -685,7 +694,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
685
694
|
/// @param token The reward token to recycle.
|
|
686
695
|
/// @param rounds The reward rounds to recycle.
|
|
687
696
|
/// @return amount The total amount recycled.
|
|
688
|
-
function
|
|
697
|
+
function _recycleExpiredRewards(
|
|
689
698
|
address hook,
|
|
690
699
|
uint256 groupId,
|
|
691
700
|
IERC20 token,
|
|
@@ -748,7 +757,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
748
757
|
// Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
|
|
749
758
|
if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
|
|
750
759
|
|
|
751
|
-
//
|
|
760
|
+
// Only the authorized holder can collateralize vesting rewards and choose the loan beneficiary.
|
|
752
761
|
_requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
|
|
753
762
|
|
|
754
763
|
// Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
|
|
@@ -1548,6 +1557,41 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1548
1557
|
/// @return canClaim True if the account can collect rewards for this token ID.
|
|
1549
1558
|
function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
|
|
1550
1559
|
|
|
1560
|
+
/// @notice The canonical beneficiary for permissionless collection of a token ID's rewards.
|
|
1561
|
+
/// @param hook The hook the token ID belongs to.
|
|
1562
|
+
/// @param tokenId The token ID to get the claim beneficiary of.
|
|
1563
|
+
/// @return beneficiary The address that helpers can collect the token ID's rewards to.
|
|
1564
|
+
function _claimBeneficiaryOf(address hook, uint256 tokenId) internal view virtual returns (address beneficiary);
|
|
1565
|
+
|
|
1566
|
+
/// @notice Revert unless the caller can collect the requested token IDs to the beneficiary.
|
|
1567
|
+
/// @dev A caller that controls a token ID can route that token ID's collected rewards anywhere. A helper that
|
|
1568
|
+
/// does not control the token ID can only collect to that token ID's canonical beneficiary.
|
|
1569
|
+
/// @param hook The hook the token IDs belong to.
|
|
1570
|
+
/// @param tokenIds The token IDs whose collected rewards will be transferred.
|
|
1571
|
+
/// @param beneficiary The address that will receive the collected rewards.
|
|
1572
|
+
function _requireCanCollectTo(address hook, uint256[] calldata tokenIds, address beneficiary) internal view {
|
|
1573
|
+
for (uint256 i; i < tokenIds.length;) {
|
|
1574
|
+
uint256 tokenId = tokenIds[i];
|
|
1575
|
+
|
|
1576
|
+
// Holders can choose any beneficiary for token IDs they control.
|
|
1577
|
+
if (!_canClaim({hook: hook, tokenId: tokenId, account: msg.sender})) {
|
|
1578
|
+
// Helpers can only send rewards to the token ID's canonical beneficiary.
|
|
1579
|
+
if (beneficiary != _claimBeneficiaryOf({hook: hook, tokenId: tokenId})) {
|
|
1580
|
+
revert JBDistributor_NoAccess({hook: hook, tokenId: tokenId, account: msg.sender});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
unchecked {
|
|
1585
|
+
++i;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/// @notice Revert unless the caller is authorized to redirect rewards or borrow against each token ID.
|
|
1591
|
+
/// @param hook The hook whose token IDs are being checked.
|
|
1592
|
+
/// @param tokenIds The token IDs to check.
|
|
1593
|
+
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
|
|
1594
|
+
|
|
1551
1595
|
/// @notice Revert if called while an inbound ERC-20 transfer is being measured.
|
|
1552
1596
|
/// @dev Reward tokens are arbitrary contracts. This guard prevents token callbacks from mutating distributor
|
|
1553
1597
|
/// accounting midway through a balance-delta measurement.
|
|
@@ -1570,13 +1614,23 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1570
1614
|
}
|
|
1571
1615
|
}
|
|
1572
1616
|
|
|
1573
|
-
/// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations,
|
|
1574
|
-
///
|
|
1617
|
+
/// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations, and
|
|
1618
|
+
/// their historical forfeited rewards can be materialized and recycled via `releaseForfeitedRewards`.
|
|
1575
1619
|
/// @param hook The hook the token belongs to.
|
|
1576
1620
|
/// @param tokenId The token ID to check.
|
|
1577
1621
|
/// @return tokenWasBurned True if the token has been burned.
|
|
1578
1622
|
function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
|
|
1579
1623
|
|
|
1624
|
+
/// @notice Validate token IDs passed to `releaseForfeitedRewards`.
|
|
1625
|
+
/// @dev Defaults to no additional validation. Concrete distributors can enforce ordering or model-specific rules
|
|
1626
|
+
/// that are not captured by `_tokenBurned`.
|
|
1627
|
+
/// @param hook The hook the token IDs belong to.
|
|
1628
|
+
/// @param tokenIds The token IDs to validate for forfeiture.
|
|
1629
|
+
function _validateForfeitedTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual {
|
|
1630
|
+
hook;
|
|
1631
|
+
tokenIds;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1580
1634
|
/// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
|
|
1581
1635
|
/// @dev Subclasses define how stake is measured.
|
|
1582
1636
|
/// @param hook The hook the token belongs to.
|
|
@@ -1600,4 +1654,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1600
1654
|
view
|
|
1601
1655
|
virtual
|
|
1602
1656
|
returns (uint256 totalStakedAmount);
|
|
1657
|
+
|
|
1658
|
+
/// @notice Revert unless each token ID is valid for this concrete distributor.
|
|
1659
|
+
/// @param hook The hook the token IDs belong to.
|
|
1660
|
+
/// @param tokenIds The token IDs to validate.
|
|
1661
|
+
function _validateTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
|
|
1603
1662
|
}
|