@bananapus/distributor-v6 0.0.37 → 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 +40 -30
- package/package.json +6 -6
- package/references/operations.md +4 -4
- package/references/runtime.md +1 -1
- package/src/JB721Distributor.sol +198 -166
- package/src/JBDistributor.sol +115 -55
- 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 +3 -2
- package/src/structs/JBClaimContext.sol +1 -0
- package/src/structs/JBRewardRoundData.sol +2 -2
- package/src/structs/JBVestContext.sol +1 -0
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
|
|
|
@@ -163,6 +164,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
163
164
|
|
|
164
165
|
/// @notice The block number recorded as the snapshot point for each round.
|
|
165
166
|
/// @dev Set to `block.number - 1` on first interaction in a round, so that `IVotes.getPastVotes` works.
|
|
167
|
+
/// @custom:param round The round whose snapshot block is being recorded.
|
|
166
168
|
mapping(uint256 round => uint256) public override roundSnapshotBlock;
|
|
167
169
|
|
|
168
170
|
/// @notice Reward data assigned to each funding round.
|
|
@@ -221,6 +223,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
221
223
|
// -------------------------- constructor ---------------------------- //
|
|
222
224
|
//*********************************************************************//
|
|
223
225
|
|
|
226
|
+
/// @notice Initializes the shared distributor configuration.
|
|
224
227
|
/// @param controller The JB controller used for token registry lookups and revnet loan permissions.
|
|
225
228
|
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
226
229
|
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
@@ -265,10 +268,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
265
268
|
//*********************************************************************//
|
|
266
269
|
|
|
267
270
|
/// @notice Begin vesting all unclaimed past reward rounds for the specified token IDs.
|
|
268
|
-
/// @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
|
|
269
272
|
/// entries that start now and unlock over `VESTING_ROUNDS`. Current-round funding is excluded until a later round
|
|
270
|
-
/// starts. The model-specific per-round claim math and
|
|
271
|
-
///
|
|
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.
|
|
272
275
|
/// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
|
|
273
276
|
/// @param tokenIds The staker token IDs to claim rewards for.
|
|
274
277
|
/// @param tokens The reward tokens to begin vesting.
|
|
@@ -295,13 +298,19 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
295
298
|
_fund({hook: hook, groupId: 0, token: token, amount: amount});
|
|
296
299
|
}
|
|
297
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
|
+
|
|
298
307
|
/// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
|
|
299
|
-
/// @dev
|
|
308
|
+
/// @dev Recycling is permissionless; any keeper or frontend can sweep an expired round.
|
|
300
309
|
/// @param hook The hook whose expired rewards should be recycled.
|
|
301
310
|
/// @param token The reward token to recycle.
|
|
302
311
|
/// @param rounds The reward rounds to recycle.
|
|
303
312
|
/// @return amount The total amount recycled.
|
|
304
|
-
function
|
|
313
|
+
function recycleExpiredRewards(
|
|
305
314
|
address hook,
|
|
306
315
|
IERC20 token,
|
|
307
316
|
uint256[] calldata rounds
|
|
@@ -311,17 +320,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
311
320
|
override
|
|
312
321
|
returns (uint256 amount)
|
|
313
322
|
{
|
|
314
|
-
amount =
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/// @notice Record the snapshot block for the current round (and eagerly for the next round). Callable by anyone —
|
|
318
|
-
/// keepers or frontends can call this early in a round to lock the snapshot block before any claims occur.
|
|
319
|
-
function poke() external override {
|
|
320
|
-
_ensureSnapshotBlock(currentRound());
|
|
323
|
+
amount = _recycleExpiredRewards({hook: hook, groupId: 0, token: token, rounds: rounds});
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
/// @notice Recycle
|
|
324
|
-
/// @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.
|
|
325
329
|
/// @param hook The hook whose tokens were burned.
|
|
326
330
|
/// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
|
|
327
331
|
/// @param tokens The reward tokens to recycle.
|
|
@@ -412,27 +416,6 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
412
416
|
// ----------------------- public transactions ----------------------- //
|
|
413
417
|
//*********************************************************************//
|
|
414
418
|
|
|
415
|
-
/// @notice Begin vesting any unclaimed past reward rounds, then collect everything that has since unlocked and
|
|
416
|
-
/// transfer it to the beneficiary — so callers don't need to separately call `beginVesting`.
|
|
417
|
-
/// @dev The model-specific per-round claim math and the authorization check live in the `_claimPastRewards`
|
|
418
|
-
/// and `_requireCanClaimTokenIds` hooks each concrete distributor implements.
|
|
419
|
-
/// @param hook The hook whose stakers are collecting.
|
|
420
|
-
/// @param tokenIds The IDs of the tokens to collect for (caller must be authorized for all of them).
|
|
421
|
-
/// @param tokens The reward tokens to collect vested amounts of.
|
|
422
|
-
/// @param beneficiary The recipient of the collected tokens.
|
|
423
|
-
function collectVestedRewards(
|
|
424
|
-
address hook,
|
|
425
|
-
uint256[] calldata tokenIds,
|
|
426
|
-
IERC20[] calldata tokens,
|
|
427
|
-
address beneficiary
|
|
428
|
-
)
|
|
429
|
-
public
|
|
430
|
-
virtual
|
|
431
|
-
override
|
|
432
|
-
{
|
|
433
|
-
_collectVestedRewards({hook: hook, groupId: 0, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
419
|
/// @notice Borrow from a revnet using one token ID's uncollected vesting rewards as collateral.
|
|
437
420
|
/// @dev The distributor keeps custody of the loan NFT. Collection is blocked until repayment restores the
|
|
438
421
|
/// collateral to the original vesting schedule.
|
|
@@ -471,6 +454,27 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
471
454
|
});
|
|
472
455
|
}
|
|
473
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
|
+
|
|
474
478
|
/// @notice Repay a distributor-held Revnet loan and restore its collateral to the original vesting schedule.
|
|
475
479
|
/// @param loanId The Revnet loan NFT ID to repay.
|
|
476
480
|
/// @param maxRepayBorrowAmount The maximum source-token amount the caller is willing to repay.
|
|
@@ -575,11 +579,6 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
575
579
|
internal
|
|
576
580
|
virtual;
|
|
577
581
|
|
|
578
|
-
/// @notice Revert unless the caller is authorized to claim each token ID.
|
|
579
|
-
/// @param hook The hook whose token IDs are being checked.
|
|
580
|
-
/// @param tokenIds The token IDs to check.
|
|
581
|
-
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
|
|
582
|
-
|
|
583
582
|
/// @notice Shared begin-vesting logic across reward groups.
|
|
584
583
|
/// @param hook The hook whose stakers are vesting.
|
|
585
584
|
/// @param groupId The reward group (0 = the default group).
|
|
@@ -599,8 +598,8 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
599
598
|
// Revert if no token IDs are provided.
|
|
600
599
|
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
601
600
|
|
|
602
|
-
//
|
|
603
|
-
|
|
601
|
+
// Validate token IDs before a permissionless helper can materialize vesting state.
|
|
602
|
+
_validateTokenIds({hook: hook, tokenIds: tokenIds});
|
|
604
603
|
|
|
605
604
|
// Materialize all unclaimed historical reward rounds into fresh vesting entries that start now.
|
|
606
605
|
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
@@ -627,19 +626,24 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
627
626
|
// Revert if no token IDs are provided.
|
|
628
627
|
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
629
628
|
|
|
630
|
-
//
|
|
631
|
-
|
|
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});
|
|
632
634
|
|
|
633
635
|
// Before collecting, bring the token IDs current by starting vesting for any past reward rounds.
|
|
634
636
|
_claimPastRewards({hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens});
|
|
635
637
|
|
|
636
|
-
// Release whatever portion of
|
|
638
|
+
// Release whatever portion of vesting entries has unlocked by this round.
|
|
637
639
|
_unlockRewards({
|
|
638
640
|
hook: hook, groupId: groupId, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true
|
|
639
641
|
});
|
|
640
642
|
}
|
|
641
643
|
|
|
642
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.
|
|
643
647
|
/// @param hook The hook whose tokens were burned.
|
|
644
648
|
/// @param groupId The reward group (0 = the default group).
|
|
645
649
|
/// @param tokenIds The IDs of the burned tokens.
|
|
@@ -657,6 +661,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
657
661
|
// Do not let reward-token callbacks mutate vesting state during inbound balance-delta accounting.
|
|
658
662
|
_requireNotAcceptingToken();
|
|
659
663
|
|
|
664
|
+
// Let concrete distributors enforce forfeiture-only validation before claim cursors can move.
|
|
665
|
+
_validateForfeitedTokenIds({hook: hook, tokenIds: tokenIds});
|
|
666
|
+
|
|
660
667
|
// Make sure that all staker token IDs are burned.
|
|
661
668
|
for (uint256 i; i < tokenIds.length;) {
|
|
662
669
|
if (!_tokenBurned({hook: hook, tokenId: tokenIds[i]})) {
|
|
@@ -667,7 +674,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
667
674
|
}
|
|
668
675
|
}
|
|
669
676
|
|
|
670
|
-
//
|
|
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.
|
|
671
681
|
_unlockRewards({
|
|
672
682
|
hook: hook,
|
|
673
683
|
groupId: groupId,
|
|
@@ -684,7 +694,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
684
694
|
/// @param token The reward token to recycle.
|
|
685
695
|
/// @param rounds The reward rounds to recycle.
|
|
686
696
|
/// @return amount The total amount recycled.
|
|
687
|
-
function
|
|
697
|
+
function _recycleExpiredRewards(
|
|
688
698
|
address hook,
|
|
689
699
|
uint256 groupId,
|
|
690
700
|
IERC20 token,
|
|
@@ -747,7 +757,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
747
757
|
// Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
|
|
748
758
|
if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
|
|
749
759
|
|
|
750
|
-
//
|
|
760
|
+
// Only the authorized holder can collateralize vesting rewards and choose the loan beneficiary.
|
|
751
761
|
_requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
|
|
752
762
|
|
|
753
763
|
// Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
|
|
@@ -911,7 +921,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
911
921
|
revert JBDistributor_UnexpectedNativeValue({msgValue: msg.value, token: loan.sourceToken});
|
|
912
922
|
}
|
|
913
923
|
|
|
914
|
-
// Pull the exact current payoff from the caller.
|
|
924
|
+
// Pull the exact current payoff from the caller. Distributor inventory must not cover a shortfall.
|
|
915
925
|
IERC20 sourceToken = IERC20(loan.sourceToken);
|
|
916
926
|
uint256 sourceBalanceBefore = sourceToken.balanceOf(address(this));
|
|
917
927
|
sourceToken.safeTransferFrom({from: msg.sender, to: address(this), value: repayBorrowAmount});
|
|
@@ -1263,7 +1273,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1263
1273
|
/// @param round The reward round.
|
|
1264
1274
|
/// @return claimDeadline The deadline timestamp. Zero means no expiration.
|
|
1265
1275
|
function _claimDeadlineFor(uint256 round) internal view returns (uint48 claimDeadline) {
|
|
1266
|
-
//
|
|
1276
|
+
// A zero claim duration means the round never expires.
|
|
1267
1277
|
if (CLAIM_DURATION == 0) return 0;
|
|
1268
1278
|
|
|
1269
1279
|
// Start the window at the next round boundary, when the funded round first becomes claimable.
|
|
@@ -1547,6 +1557,41 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1547
1557
|
/// @return canClaim True if the account can collect rewards for this token ID.
|
|
1548
1558
|
function _canClaim(address hook, uint256 tokenId, address account) internal view virtual returns (bool canClaim);
|
|
1549
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
|
+
|
|
1550
1595
|
/// @notice Revert if called while an inbound ERC-20 transfer is being measured.
|
|
1551
1596
|
/// @dev Reward tokens are arbitrary contracts. This guard prevents token callbacks from mutating distributor
|
|
1552
1597
|
/// accounting midway through a balance-delta measurement.
|
|
@@ -1569,13 +1614,23 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1569
1614
|
}
|
|
1570
1615
|
}
|
|
1571
1616
|
|
|
1572
|
-
/// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations,
|
|
1573
|
-
///
|
|
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`.
|
|
1574
1619
|
/// @param hook The hook the token belongs to.
|
|
1575
1620
|
/// @param tokenId The token ID to check.
|
|
1576
1621
|
/// @return tokenWasBurned True if the token has been burned.
|
|
1577
1622
|
function _tokenBurned(address hook, uint256 tokenId) internal view virtual returns (bool tokenWasBurned);
|
|
1578
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
|
+
|
|
1579
1634
|
/// @notice The stake weight of a specific token ID, used to calculate its pro-rata share of distributions.
|
|
1580
1635
|
/// @dev Subclasses define how stake is measured.
|
|
1581
1636
|
/// @param hook The hook the token belongs to.
|
|
@@ -1599,4 +1654,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1599
1654
|
view
|
|
1600
1655
|
virtual
|
|
1601
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;
|
|
1602
1662
|
}
|