@bananapus/distributor-v6 0.0.27 → 0.0.29
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 +19 -7
- package/foundry.toml +1 -0
- package/package.json +3 -2
- package/script/Deploy.s.sol +20 -1
- package/src/JB721Distributor.sol +27 -21
- package/src/JBDistributor.sol +679 -107
- package/src/JBTokenDistributor.sol +18 -7
- package/src/interfaces/IJBDistributor.sol +140 -17
- package/src/structs/JBBorrowContext.sol +24 -0
- package/src/structs/JBVestingLoan.sol +18 -0
package/src/JBDistributor.sol
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
5
|
+
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
6
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
7
|
+
import {IJBToken} from "@bananapus/core-v6/src/interfaces/IJBToken.sol";
|
|
4
8
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
9
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
10
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
11
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
5
12
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
6
13
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
7
14
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
15
|
+
import {IREVLoans} from "@rev-net/core-v6/src/interfaces/IREVLoans.sol";
|
|
16
|
+
import {IREVOwner} from "@rev-net/core-v6/src/interfaces/IREVOwner.sol";
|
|
17
|
+
import {REVLoan} from "@rev-net/core-v6/src/structs/REVLoan.sol";
|
|
8
18
|
|
|
9
19
|
import {IJBDistributor} from "./interfaces/IJBDistributor.sol";
|
|
10
20
|
import {JBVestingMath} from "./libraries/JBVestingMath.sol";
|
|
21
|
+
import {JBBorrowContext} from "./structs/JBBorrowContext.sol";
|
|
11
22
|
import {JBRewardRoundData} from "./structs/JBRewardRoundData.sol";
|
|
12
23
|
import {JBTokenSnapshotData} from "./structs/JBTokenSnapshotData.sol";
|
|
13
24
|
import {JBVestingData} from "./structs/JBVestingData.sol";
|
|
25
|
+
import {JBVestingLoan} from "./structs/JBVestingLoan.sol";
|
|
14
26
|
|
|
15
27
|
/// @notice Abstract base for reward distributors. Manages round-based distribution of ERC-20 tokens (or native ETH)
|
|
16
28
|
/// to stakers with linear vesting. Each round, a snapshot is taken of the distributable balance, and stakers can
|
|
17
29
|
/// claim their pro-rata share based on their stake weight at the snapshot block. Claimed tokens vest linearly over
|
|
18
|
-
/// `
|
|
30
|
+
/// `VESTING_ROUNDS` rounds and can be collected as they unlock.
|
|
19
31
|
/// @dev Subclasses define how stake is measured (`_tokenStake`, `_totalStake`), who can claim (`_canClaim`), and
|
|
20
32
|
/// what "burned" means (`_tokenBurned`). Two concrete implementations exist: `JBTokenDistributor` (IVotes tokens)
|
|
21
33
|
/// and `JB721Distributor` (Juicebox 721 NFTs).
|
|
@@ -29,6 +41,15 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
29
41
|
/// @notice Thrown when an empty tokenIds array is passed.
|
|
30
42
|
error JBDistributor_EmptyTokenIds(uint256 tokenIdCount);
|
|
31
43
|
|
|
44
|
+
/// @notice Thrown when a repaid Revnet loan returned less collateral than it originally borrowed.
|
|
45
|
+
error JBDistributor_InsufficientRepaidCollateral(uint256 expectedAmount, uint256 actualAmount);
|
|
46
|
+
|
|
47
|
+
/// @notice Thrown when the provided repayment amount is less than the amount needed to repay a loan.
|
|
48
|
+
error JBDistributor_InsufficientRepayAmount(uint256 amount, uint256 requiredAmount);
|
|
49
|
+
|
|
50
|
+
/// @notice Thrown when the Revnet loans contract returns a reserved loan ID.
|
|
51
|
+
error JBDistributor_InvalidVestingLoanId(uint256 loanId);
|
|
52
|
+
|
|
32
53
|
/// @notice Thrown when the round duration is zero.
|
|
33
54
|
error JBDistributor_InvalidRoundDuration(uint256 roundDuration);
|
|
34
55
|
|
|
@@ -41,21 +62,45 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
41
62
|
/// @notice Thrown when there is nothing to distribute for a token in the current round.
|
|
42
63
|
error JBDistributor_NothingToDistribute(address hook, address token, uint256 round);
|
|
43
64
|
|
|
65
|
+
/// @notice Thrown when there are no uncollected vesting revnet tokens to collateralize a loan.
|
|
66
|
+
error JBDistributor_NothingToBorrow(address hook, address token);
|
|
67
|
+
|
|
68
|
+
/// @notice Thrown when a loan ID is not tracking distributor-owned vesting collateral.
|
|
69
|
+
error JBDistributor_NoVestingLoan(uint256 loanId);
|
|
70
|
+
|
|
71
|
+
/// @notice Thrown when a reward token is not a revnet token owned by the configured REVOwner.
|
|
72
|
+
error JBDistributor_NotRevnetRewardToken(address token);
|
|
73
|
+
|
|
44
74
|
/// @notice Thrown when an ERC-20 reenters a funding balance-delta measurement.
|
|
45
75
|
error JBDistributor_ReentrantTokenTransfer(address token);
|
|
46
76
|
|
|
77
|
+
/// @notice Thrown when revnet loan-backed collection has not been configured.
|
|
78
|
+
error JBDistributor_RevnetLoansNotConfigured();
|
|
79
|
+
|
|
47
80
|
/// @notice Thrown when unexpected native ETH is sent with an ERC-20 operation.
|
|
48
81
|
error JBDistributor_UnexpectedNativeValue(uint256 msgValue, address token);
|
|
49
82
|
|
|
83
|
+
/// @notice Thrown when a function requires exactly one reward token.
|
|
84
|
+
error JBDistributor_UnexpectedTokenCount(uint256 tokenCount);
|
|
85
|
+
|
|
86
|
+
/// @notice Thrown when a token ID has an outstanding loan against its vesting rewards.
|
|
87
|
+
error JBDistributor_VestingLoanOutstanding(address hook, uint256 tokenId, address token, uint256 loanId);
|
|
88
|
+
|
|
89
|
+
/// @notice Thrown when a vesting loan is written off before Revnet has liquidated it.
|
|
90
|
+
error JBDistributor_VestingLoanNotLiquidated(uint256 loanId);
|
|
91
|
+
|
|
92
|
+
/// @notice Thrown when vesting loans are requested from a distributor with no vesting period.
|
|
93
|
+
error JBDistributor_VestingLoansDisabled();
|
|
94
|
+
|
|
95
|
+
/// @notice Thrown when rewards cannot be burned by the JB controller.
|
|
96
|
+
error JBDistributor_TokenNotBurnable(address token);
|
|
97
|
+
|
|
50
98
|
/// @notice Thrown when a value cannot fit in a uint208 reward-round field.
|
|
51
99
|
error JBDistributor_Uint208Overflow(uint256 value);
|
|
52
100
|
|
|
53
|
-
/// @notice Thrown when a value cannot fit in a uint48
|
|
101
|
+
/// @notice Thrown when a value cannot fit in a uint48 field.
|
|
54
102
|
error JBDistributor_Uint48Overflow(uint256 value);
|
|
55
103
|
|
|
56
|
-
/// @notice Thrown when fundings in the same reward round use different claim deadlines.
|
|
57
|
-
error JBDistributor_ClaimDeadlineMismatch(uint256 existingDeadline, uint256 newDeadline);
|
|
58
|
-
|
|
59
104
|
//*********************************************************************//
|
|
60
105
|
// ------------------------- public constants ------------------------ //
|
|
61
106
|
//*********************************************************************//
|
|
@@ -63,26 +108,51 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
63
108
|
/// @notice The number of shares that represent 100%.
|
|
64
109
|
uint256 public constant MAX_SHARE = 100_000;
|
|
65
110
|
|
|
66
|
-
|
|
67
|
-
|
|
111
|
+
//*********************************************************************//
|
|
112
|
+
// ----------------------- internal constants ------------------------ //
|
|
113
|
+
//*********************************************************************//
|
|
114
|
+
|
|
115
|
+
/// @notice Sentinel used before `REV_LOANS.borrowFrom` returns the real loan ID.
|
|
116
|
+
uint256 internal constant _PENDING_VESTING_LOAN_ID = type(uint256).max;
|
|
68
117
|
|
|
69
118
|
//*********************************************************************//
|
|
70
119
|
// ---------------- public immutable stored properties --------------- //
|
|
71
120
|
//*********************************************************************//
|
|
72
121
|
|
|
122
|
+
/// @notice The number of seconds after a reward round becomes claimable before unclaimed rewards expire.
|
|
123
|
+
/// @dev A zero duration means reward rounds do not expire.
|
|
124
|
+
uint48 public immutable override CLAIM_DURATION;
|
|
125
|
+
|
|
126
|
+
/// @notice The JB controller used to burn expired or forfeited project-token rewards.
|
|
127
|
+
IJBController public immutable override CONTROLLER;
|
|
128
|
+
|
|
73
129
|
/// @notice The duration of each round, specified in seconds.
|
|
74
|
-
uint256 public immutable override
|
|
130
|
+
uint256 public immutable override ROUND_DURATION;
|
|
131
|
+
|
|
132
|
+
/// @notice The Revnet loans contract used to borrow against vested revnet rewards.
|
|
133
|
+
IREVLoans public immutable override REV_LOANS;
|
|
134
|
+
|
|
135
|
+
/// @notice The REVOwner contract that must own a reward token's project to enable loan-backed collection.
|
|
136
|
+
IREVOwner public immutable override REV_OWNER;
|
|
75
137
|
|
|
76
138
|
/// @notice The starting timestamp of the distributor.
|
|
77
|
-
uint256 public immutable
|
|
139
|
+
uint256 public immutable override STARTING_TIMESTAMP;
|
|
78
140
|
|
|
79
141
|
/// @notice The number of rounds until tokens are fully vested.
|
|
80
|
-
uint256 public immutable override
|
|
142
|
+
uint256 public immutable override VESTING_ROUNDS;
|
|
81
143
|
|
|
82
144
|
//*********************************************************************//
|
|
83
145
|
// --------------------- public stored properties -------------------- //
|
|
84
146
|
//*********************************************************************//
|
|
85
147
|
|
|
148
|
+
/// @notice The active Revnet loan using one token ID's vesting rewards as collateral.
|
|
149
|
+
/// @custom:param hook The hook the token ID belongs to.
|
|
150
|
+
/// @custom:param tokenId The token ID whose vesting rewards are collateralized.
|
|
151
|
+
/// @custom:param token The reward token used as loan collateral.
|
|
152
|
+
mapping(address hook => mapping(uint256 tokenId => mapping(IERC20 token => uint256)))
|
|
153
|
+
public
|
|
154
|
+
override activeVestingLoanIdOf;
|
|
155
|
+
|
|
86
156
|
/// @notice The index within `vestingDataOf` of the latest vest.
|
|
87
157
|
/// @custom:param hook The hook the tokenId belongs to.
|
|
88
158
|
/// @custom:param tokenId The ID of the token to which the vests belong.
|
|
@@ -104,6 +174,11 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
104
174
|
/// @custom:param token The address of the token that is vesting.
|
|
105
175
|
mapping(address hook => mapping(IERC20 token => uint256 amount)) public override totalVestingAmountOf;
|
|
106
176
|
|
|
177
|
+
/// @notice The amount of vesting inventory currently collateralized in Revnet loans.
|
|
178
|
+
/// @custom:param hook The hook whose stakers own the vesting rewards.
|
|
179
|
+
/// @custom:param token The reward token used as loan collateral.
|
|
180
|
+
mapping(address hook => mapping(IERC20 token => uint256 amount)) public override totalLoanedVestingAmountOf;
|
|
181
|
+
|
|
107
182
|
/// @notice All vesting data of a tokenId for any number of vesting tokens.
|
|
108
183
|
/// @custom:param hook The hook the tokenId belongs to.
|
|
109
184
|
/// @custom:param tokenId The ID of the token to which the vests belong.
|
|
@@ -123,6 +198,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
123
198
|
/// @custom:param token The token to check the balance of.
|
|
124
199
|
mapping(address hook => mapping(IERC20 token => uint256)) internal _balanceOf;
|
|
125
200
|
|
|
201
|
+
/// @notice The vesting position collateralized by a Revnet loan.
|
|
202
|
+
/// @custom:param loanId The Revnet loan NFT ID.
|
|
203
|
+
mapping(uint256 loanId => JBVestingLoan) internal _vestingLoanOf;
|
|
204
|
+
|
|
126
205
|
/// @notice The snapshot data of the token information for each round.
|
|
127
206
|
/// @custom:param hook The hook the snapshot is for.
|
|
128
207
|
/// @custom:param token The address of the token claimed and vested.
|
|
@@ -149,15 +228,43 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
149
228
|
// -------------------------- constructor ---------------------------- //
|
|
150
229
|
//*********************************************************************//
|
|
151
230
|
|
|
231
|
+
/// @param controller The JB controller used to burn expired or forfeited project-token rewards.
|
|
232
|
+
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
233
|
+
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
152
234
|
/// @param initialRoundDuration The duration of each round, specified in seconds.
|
|
153
235
|
/// @param initialVestingRounds The number of rounds until tokens are fully vested.
|
|
154
|
-
|
|
236
|
+
/// @param initialClaimDuration The number of seconds claimants have after each reward round becomes claimable.
|
|
237
|
+
constructor(
|
|
238
|
+
IJBController controller,
|
|
239
|
+
IREVLoans revLoans,
|
|
240
|
+
IREVOwner revOwner,
|
|
241
|
+
uint256 initialRoundDuration,
|
|
242
|
+
uint256 initialVestingRounds,
|
|
243
|
+
uint48 initialClaimDuration
|
|
244
|
+
) {
|
|
155
245
|
if (initialRoundDuration == 0) {
|
|
156
246
|
revert JBDistributor_InvalidRoundDuration({roundDuration: initialRoundDuration});
|
|
157
247
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
248
|
+
CLAIM_DURATION = initialClaimDuration;
|
|
249
|
+
CONTROLLER = controller;
|
|
250
|
+
REV_LOANS = revLoans;
|
|
251
|
+
REV_OWNER = revOwner;
|
|
252
|
+
STARTING_TIMESTAMP = block.timestamp;
|
|
253
|
+
ROUND_DURATION = initialRoundDuration;
|
|
254
|
+
VESTING_ROUNDS = initialVestingRounds;
|
|
255
|
+
|
|
256
|
+
// Let the trusted Revnet loans contract burn this distributor's project-token rewards as collateral.
|
|
257
|
+
if (address(revLoans) != address(0)) {
|
|
258
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
259
|
+
permissionIds[0] = JBPermissionIds.BURN_TOKENS;
|
|
260
|
+
IJBPermissions permissions = IJBPermissioned(address(controller)).PERMISSIONS();
|
|
261
|
+
permissions.setPermissionsFor({
|
|
262
|
+
account: address(this),
|
|
263
|
+
permissionsData: JBPermissionsData({
|
|
264
|
+
operator: address(revLoans), projectId: 0, permissionIds: permissionIds
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
}
|
|
161
268
|
}
|
|
162
269
|
|
|
163
270
|
//*********************************************************************//
|
|
@@ -166,7 +273,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
166
273
|
|
|
167
274
|
/// @notice Snapshot the current round's distributable balance and begin vesting for the specified token IDs.
|
|
168
275
|
/// Each token ID's share is proportional to its stake weight relative to the total stake at the snapshot block.
|
|
169
|
-
/// Vesting completes after `
|
|
276
|
+
/// Vesting completes after `VESTING_ROUNDS` rounds. Reverts if there's nothing to distribute.
|
|
170
277
|
/// @param hook The hook (IVotes token or 721 hook) whose stakers are vesting.
|
|
171
278
|
/// @param tokenIds The staker token IDs to claim rewards for.
|
|
172
279
|
/// @param tokens The reward tokens to begin vesting.
|
|
@@ -219,7 +326,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
219
326
|
token: token,
|
|
220
327
|
distributable: distributable,
|
|
221
328
|
totalStakeAmount: totalStakeAmount,
|
|
222
|
-
vestingReleaseRound: round +
|
|
329
|
+
vestingReleaseRound: round + VESTING_ROUNDS
|
|
223
330
|
});
|
|
224
331
|
|
|
225
332
|
unchecked {
|
|
@@ -239,27 +346,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
239
346
|
/// @param token The token to fund with.
|
|
240
347
|
/// @param amount The amount to fund (ignored for native ETH — `msg.value` is used instead).
|
|
241
348
|
function fund(address hook, IERC20 token, uint256 amount) external payable virtual override {
|
|
242
|
-
_fund({hook: hook, token: token, amount: amount
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/// @notice Fund the distributor for a specific hook with expiring rewards.
|
|
246
|
-
/// @dev The claim window starts when the funded round first becomes claimable.
|
|
247
|
-
/// @param hook The hook to fund.
|
|
248
|
-
/// @param token The token to fund with.
|
|
249
|
-
/// @param amount The amount to fund.
|
|
250
|
-
/// @param claimDuration The number of seconds claimants have after the round becomes claimable.
|
|
251
|
-
function fundWithClaimDuration(
|
|
252
|
-
address hook,
|
|
253
|
-
IERC20 token,
|
|
254
|
-
uint256 amount,
|
|
255
|
-
uint48 claimDuration
|
|
256
|
-
)
|
|
257
|
-
external
|
|
258
|
-
payable
|
|
259
|
-
virtual
|
|
260
|
-
override
|
|
261
|
-
{
|
|
262
|
-
_fund({hook: hook, token: token, amount: amount, claimDuration: claimDuration});
|
|
349
|
+
_fund({hook: hook, token: token, amount: amount});
|
|
263
350
|
}
|
|
264
351
|
|
|
265
352
|
/// @notice Burn unclaimed rewards from expired reward rounds.
|
|
@@ -298,13 +385,13 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
298
385
|
_ensureSnapshotBlock(currentRound());
|
|
299
386
|
}
|
|
300
387
|
|
|
301
|
-
/// @notice
|
|
302
|
-
///
|
|
303
|
-
///
|
|
388
|
+
/// @notice Burn unlocked rewards tied to burned tokens. When an NFT is burned, its pending vesting entries become
|
|
389
|
+
/// stranded — this function unlocks and burns them instead of sending them to the beneficiary. Anyone can call
|
|
390
|
+
/// this for burned tokens.
|
|
304
391
|
/// @param hook The hook whose tokens were burned.
|
|
305
392
|
/// @param tokenIds The IDs of the burned tokens (reverts if any are not actually burned).
|
|
306
393
|
/// @param tokens The reward tokens to release.
|
|
307
|
-
/// @param beneficiary Unused for forfeiture — tokens
|
|
394
|
+
/// @param beneficiary Unused for forfeiture — tokens are burned. Kept for interface compatibility.
|
|
308
395
|
function releaseForfeitedRewards(
|
|
309
396
|
address hook,
|
|
310
397
|
uint256[] calldata tokenIds,
|
|
@@ -327,7 +414,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
327
414
|
}
|
|
328
415
|
}
|
|
329
416
|
|
|
330
|
-
// Unlock the rewards and
|
|
417
|
+
// Unlock the rewards and burn the forfeited amount.
|
|
331
418
|
_unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: false});
|
|
332
419
|
}
|
|
333
420
|
|
|
@@ -358,25 +445,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
358
445
|
override
|
|
359
446
|
returns (uint256 tokenAmount)
|
|
360
447
|
{
|
|
361
|
-
|
|
362
|
-
uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
|
|
363
|
-
|
|
364
|
-
// Keep a reference to the number of vesting rounds for the tokenId and token.
|
|
365
|
-
uint256 numberOfVestingRounds = vestingDataOf[hook][tokenId][token].length;
|
|
366
|
-
|
|
367
|
-
while (vestedIndex < numberOfVestingRounds) {
|
|
368
|
-
// Keep a reference to the vested data being iterated on.
|
|
369
|
-
JBVestingData memory vesting = vestingDataOf[hook][tokenId][token][vestedIndex];
|
|
370
|
-
|
|
371
|
-
// Use `original - alreadyPaid` to include rounding dust in the remaining amount.
|
|
372
|
-
tokenAmount += JBVestingMath.unclaimedAmountOf({
|
|
373
|
-
amount: vesting.amount, shareClaimed: vesting.shareClaimed, maxShare: MAX_SHARE
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
unchecked {
|
|
377
|
-
++vestedIndex;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
448
|
+
tokenAmount = _unclaimedVestingAmountOf({hook: hook, tokenId: tokenId, token: token});
|
|
380
449
|
}
|
|
381
450
|
|
|
382
451
|
/// @notice Calculate how much of a reward token is currently unlocked and ready to be collected for a given
|
|
@@ -395,6 +464,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
395
464
|
override
|
|
396
465
|
returns (uint256 tokenAmount)
|
|
397
466
|
{
|
|
467
|
+
// A loan keeps this token ID's vesting rewards in collateral custody until the loan is repaid.
|
|
468
|
+
if (activeVestingLoanIdOf[hook][tokenId][token] != 0) return 0;
|
|
469
|
+
|
|
398
470
|
// The round that we are in right now.
|
|
399
471
|
uint256 round = currentRound();
|
|
400
472
|
|
|
@@ -413,7 +485,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
413
485
|
lockedShare = JBVestingMath.lockedShareOf({
|
|
414
486
|
releaseRound: vesting.releaseRound,
|
|
415
487
|
currentRound: round,
|
|
416
|
-
vestingRounds:
|
|
488
|
+
vestingRounds: VESTING_ROUNDS,
|
|
417
489
|
maxShare: MAX_SHARE
|
|
418
490
|
});
|
|
419
491
|
|
|
@@ -450,19 +522,25 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
450
522
|
return _snapshotAtRoundOf[hook][token][round];
|
|
451
523
|
}
|
|
452
524
|
|
|
525
|
+
/// @notice The vesting position collateralized by a Revnet loan.
|
|
526
|
+
/// @param loanId The Revnet loan NFT ID.
|
|
527
|
+
function vestingLoanOf(uint256 loanId) external view override returns (JBVestingLoan memory) {
|
|
528
|
+
return _vestingLoanOf[loanId];
|
|
529
|
+
}
|
|
530
|
+
|
|
453
531
|
//*********************************************************************//
|
|
454
532
|
// -------------------------- public views --------------------------- //
|
|
455
533
|
//*********************************************************************//
|
|
456
534
|
|
|
457
535
|
/// @notice The number of the current round.
|
|
458
536
|
function currentRound() public view override returns (uint256) {
|
|
459
|
-
return (block.timestamp -
|
|
537
|
+
return (block.timestamp - STARTING_TIMESTAMP) / ROUND_DURATION;
|
|
460
538
|
}
|
|
461
539
|
|
|
462
540
|
/// @notice The timestamp at which a round started.
|
|
463
541
|
/// @param round The round to get the start timestamp of.
|
|
464
542
|
function roundStartTimestamp(uint256 round) public view override returns (uint256) {
|
|
465
|
-
return
|
|
543
|
+
return STARTING_TIMESTAMP + ROUND_DURATION * round;
|
|
466
544
|
}
|
|
467
545
|
|
|
468
546
|
//*********************************************************************//
|
|
@@ -528,7 +606,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
528
606
|
token: token,
|
|
529
607
|
distributable: distributable,
|
|
530
608
|
totalStakeAmount: totalStakeAmount,
|
|
531
|
-
vestingReleaseRound: round +
|
|
609
|
+
vestingReleaseRound: round + VESTING_ROUNDS
|
|
532
610
|
});
|
|
533
611
|
|
|
534
612
|
unchecked {
|
|
@@ -545,10 +623,433 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
545
623
|
_unlockRewards({hook: hook, tokenIds: tokenIds, tokens: tokens, beneficiary: beneficiary, ownerClaim: true});
|
|
546
624
|
}
|
|
547
625
|
|
|
626
|
+
/// @notice Borrow from a revnet using one token ID's uncollected vesting rewards as collateral.
|
|
627
|
+
/// @dev The distributor keeps custody of the loan NFT. Collection is blocked until repayment restores the
|
|
628
|
+
/// collateral to the original vesting schedule.
|
|
629
|
+
/// @param hook The hook whose staker is borrowing against vesting rewards.
|
|
630
|
+
/// @param tokenIds The single token ID to borrow against.
|
|
631
|
+
/// @param tokens The single revnet reward token to collateralize.
|
|
632
|
+
/// @param sourceToken The token to borrow from the revnet.
|
|
633
|
+
/// @param minBorrowAmount The minimum amount to borrow, denominated in `sourceToken`.
|
|
634
|
+
/// @param prepaidFeePercent The fee percent to charge upfront.
|
|
635
|
+
/// @param beneficiary The recipient of the borrowed funds.
|
|
636
|
+
/// @return loanId The Revnet loan NFT ID held by this distributor.
|
|
637
|
+
/// @return collateralCount The amount of vesting rewards used as collateral.
|
|
638
|
+
function borrowAgainstVesting(
|
|
639
|
+
address hook,
|
|
640
|
+
uint256[] calldata tokenIds,
|
|
641
|
+
IERC20[] calldata tokens,
|
|
642
|
+
address sourceToken,
|
|
643
|
+
uint256 minBorrowAmount,
|
|
644
|
+
uint256 prepaidFeePercent,
|
|
645
|
+
address payable beneficiary
|
|
646
|
+
)
|
|
647
|
+
public
|
|
648
|
+
virtual
|
|
649
|
+
override
|
|
650
|
+
returns (uint256 loanId, uint256 collateralCount)
|
|
651
|
+
{
|
|
652
|
+
// Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
|
|
653
|
+
_requireNotAcceptingToken();
|
|
654
|
+
|
|
655
|
+
// Revert if no token IDs are provided.
|
|
656
|
+
if (tokenIds.length == 0) revert JBDistributor_EmptyTokenIds({tokenIdCount: tokenIds.length});
|
|
657
|
+
|
|
658
|
+
// One distributor-held Revnet loan tracks one token ID so one repayment restores one vesting schedule.
|
|
659
|
+
if (tokenIds.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokenIds.length});
|
|
660
|
+
|
|
661
|
+
// One loan collateralizes one revnet reward token.
|
|
662
|
+
if (tokens.length != 1) revert JBDistributor_UnexpectedTokenCount({tokenCount: tokens.length});
|
|
663
|
+
|
|
664
|
+
// Zero vesting means rewards are immediately collectible, so there is no locked position to borrow against.
|
|
665
|
+
if (VESTING_ROUNDS == 0) revert JBDistributor_VestingLoansDisabled();
|
|
666
|
+
|
|
667
|
+
// Revnet loan-backed collection is disabled unless a trusted loans contract was set at deployment.
|
|
668
|
+
if (address(REV_LOANS) == address(0)) revert JBDistributor_RevnetLoansNotConfigured();
|
|
669
|
+
|
|
670
|
+
// Make sure that all tokens can be claimed by this sender.
|
|
671
|
+
_requireCanClaimTokenIds({hook: hook, tokenIds: tokenIds});
|
|
672
|
+
|
|
673
|
+
// Bundle the remaining borrow parameters to keep the loan workflow readable and stack-safe.
|
|
674
|
+
JBBorrowContext memory ctx = JBBorrowContext({
|
|
675
|
+
hook: hook,
|
|
676
|
+
tokenId: tokenIds[0],
|
|
677
|
+
token: tokens[0],
|
|
678
|
+
sourceToken: sourceToken,
|
|
679
|
+
minBorrowAmount: minBorrowAmount,
|
|
680
|
+
prepaidFeePercent: prepaidFeePercent,
|
|
681
|
+
beneficiary: beneficiary,
|
|
682
|
+
revnetId: _revnetIdOf(tokens[0])
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// Open and track the distributor-owned loan.
|
|
686
|
+
(loanId, collateralCount) = _borrowAgainstVesting({ctx: ctx, tokenIds: tokenIds, tokens: tokens});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/// @notice Repay a distributor-held Revnet loan and restore its collateral to the original vesting schedule.
|
|
690
|
+
/// @param loanId The Revnet loan NFT ID to repay.
|
|
691
|
+
/// @param maxRepayBorrowAmount The maximum source-token amount the caller is willing to repay.
|
|
692
|
+
/// @return paidOffLoanId The paid-off loan ID returned by Revnet loans.
|
|
693
|
+
function repayVestingLoan(
|
|
694
|
+
uint256 loanId,
|
|
695
|
+
uint256 maxRepayBorrowAmount
|
|
696
|
+
)
|
|
697
|
+
public
|
|
698
|
+
payable
|
|
699
|
+
virtual
|
|
700
|
+
override
|
|
701
|
+
returns (uint256 paidOffLoanId)
|
|
702
|
+
{
|
|
703
|
+
// Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
|
|
704
|
+
_requireNotAcceptingToken();
|
|
705
|
+
|
|
706
|
+
// Load the vesting position that this distributor locked when it opened the loan.
|
|
707
|
+
JBVestingLoan memory vestingLoan = _vestingLoanOf[loanId];
|
|
708
|
+
if (vestingLoan.hook == address(0)) revert JBDistributor_NoVestingLoan({loanId: loanId});
|
|
709
|
+
|
|
710
|
+
// Use Revnet's current fee quote to determine the amount needed to repay this loan now.
|
|
711
|
+
REVLoan memory loan = REV_LOANS.loanOf(loanId);
|
|
712
|
+
uint256 repayBorrowAmount =
|
|
713
|
+
uint256(loan.amount) + REV_LOANS.determineSourceFeeAmount({loan: loan, amount: loan.amount});
|
|
714
|
+
|
|
715
|
+
// Respect the caller's maximum repayment limit.
|
|
716
|
+
if (repayBorrowAmount > maxRepayBorrowAmount) {
|
|
717
|
+
revert JBDistributor_InsufficientRepayAmount({
|
|
718
|
+
amount: maxRepayBorrowAmount, requiredAmount: repayBorrowAmount
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Measure any returned project tokens while excluding any source-token payment effects.
|
|
723
|
+
uint256 rewardBalanceBefore = vestingLoan.token.balanceOf(address(this));
|
|
724
|
+
|
|
725
|
+
// Repay through this distributor because it owns the loan NFT and must receive the returned collateral.
|
|
726
|
+
paidOffLoanId = _repayLoanSource({
|
|
727
|
+
loanId: loanId,
|
|
728
|
+
loan: loan,
|
|
729
|
+
repayBorrowAmount: repayBorrowAmount,
|
|
730
|
+
collateralCount: vestingLoan.collateralCount
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// Restore the collateral to inventory while preserving the original vesting data untouched.
|
|
734
|
+
_restoreVestingCollateral({
|
|
735
|
+
loanId: loanId,
|
|
736
|
+
paidOffLoanId: paidOffLoanId,
|
|
737
|
+
vestingLoan: vestingLoan,
|
|
738
|
+
rewardBalanceBefore: rewardBalanceBefore,
|
|
739
|
+
repayBorrowAmount: repayBorrowAmount
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/// @notice Write off a distributor-held Revnet loan after Revnet liquidation permanently destroys its collateral.
|
|
744
|
+
/// @param loanId The liquidated Revnet loan NFT ID.
|
|
745
|
+
/// @return collateralCount The amount of vesting rewards forfeited by liquidation.
|
|
746
|
+
function writeOffLiquidatedVestingLoan(uint256 loanId) public virtual override returns (uint256 collateralCount) {
|
|
747
|
+
// Do not let reward-token callbacks mutate claim accounting during an inbound transfer.
|
|
748
|
+
_requireNotAcceptingToken();
|
|
749
|
+
|
|
750
|
+
// Load the distributor-local position that was locked when the loan opened.
|
|
751
|
+
JBVestingLoan memory vestingLoan = _vestingLoanOf[loanId];
|
|
752
|
+
|
|
753
|
+
// Only distributor-tracked vesting loans can be written off.
|
|
754
|
+
if (vestingLoan.hook == address(0)) revert JBDistributor_NoVestingLoan({loanId: loanId});
|
|
755
|
+
|
|
756
|
+
// Revnet liquidation deletes the loan data. A live loan can still be repaid, so do not write it off.
|
|
757
|
+
if (REV_LOANS.loanOf(loanId).createdAt != 0) revert JBDistributor_VestingLoanNotLiquidated({loanId: loanId});
|
|
758
|
+
|
|
759
|
+
// Clear the stale distributor lock and forfeit only the collateralized vesting entries.
|
|
760
|
+
collateralCount = _writeOffLiquidatedVestingLoan({loanId: loanId, vestingLoan: vestingLoan});
|
|
761
|
+
}
|
|
762
|
+
|
|
548
763
|
//*********************************************************************//
|
|
549
764
|
// ---------------------- internal transactions ---------------------- //
|
|
550
765
|
//*********************************************************************//
|
|
551
766
|
|
|
767
|
+
/// @notice Claim all past reward rounds for the given token IDs and reward tokens into fresh vesting entries.
|
|
768
|
+
/// @param hook The hook whose stakers are claiming.
|
|
769
|
+
/// @param tokenIds The token IDs to claim for.
|
|
770
|
+
/// @param tokens The reward tokens to claim.
|
|
771
|
+
function _claimPastRewards(address hook, uint256[] calldata tokenIds, IERC20[] calldata tokens) internal virtual;
|
|
772
|
+
|
|
773
|
+
/// @notice Revert unless the caller is authorized to claim each token ID.
|
|
774
|
+
/// @param hook The hook whose token IDs are being checked.
|
|
775
|
+
/// @param tokenIds The token IDs to check.
|
|
776
|
+
function _requireCanClaimTokenIds(address hook, uint256[] calldata tokenIds) internal view virtual;
|
|
777
|
+
|
|
778
|
+
/// @notice Open and track a distributor-held Revnet loan against one vesting position.
|
|
779
|
+
/// @param ctx The borrow context.
|
|
780
|
+
/// @param tokenIds The single token ID being collateralized.
|
|
781
|
+
/// @param tokens The single reward token being collateralized.
|
|
782
|
+
/// @return loanId The Revnet loan NFT ID held by this distributor.
|
|
783
|
+
/// @return collateralCount The amount of vesting rewards used as collateral.
|
|
784
|
+
function _borrowAgainstVesting(
|
|
785
|
+
JBBorrowContext memory ctx,
|
|
786
|
+
uint256[] calldata tokenIds,
|
|
787
|
+
IERC20[] calldata tokens
|
|
788
|
+
)
|
|
789
|
+
internal
|
|
790
|
+
returns (uint256 loanId, uint256 collateralCount)
|
|
791
|
+
{
|
|
792
|
+
// One vesting position cannot be collateralized by two outstanding loans.
|
|
793
|
+
uint256 activeLoanId = activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token];
|
|
794
|
+
if (activeLoanId != 0) {
|
|
795
|
+
revert JBDistributor_VestingLoanOutstanding({
|
|
796
|
+
hook: ctx.hook, tokenId: ctx.tokenId, token: address(ctx.token), loanId: activeLoanId
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Bring the claimant current before measuring collateral.
|
|
801
|
+
_claimPastRewards({hook: ctx.hook, tokenIds: tokenIds, tokens: tokens});
|
|
802
|
+
|
|
803
|
+
// Use the remaining uncollected vesting amount as collateral without advancing the vesting schedule.
|
|
804
|
+
collateralCount = _unclaimedVestingAmountOf({hook: ctx.hook, tokenId: ctx.tokenId, token: ctx.token});
|
|
805
|
+
|
|
806
|
+
// Remember the vesting-entry boundary so liquidation write-off cannot consume later rewards.
|
|
807
|
+
uint48 vestingDataCount = _toUint48(vestingDataOf[ctx.hook][ctx.tokenId][ctx.token].length);
|
|
808
|
+
|
|
809
|
+
// A zero-collateral loan would revert in Revnet, but this local error explains why.
|
|
810
|
+
if (collateralCount == 0) {
|
|
811
|
+
revert JBDistributor_NothingToBorrow({hook: ctx.hook, token: address(ctx.token)});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// The collateralized tokens leave the hook's distributable inventory.
|
|
815
|
+
_balanceOf[ctx.hook][ctx.token] -= collateralCount;
|
|
816
|
+
_accountedBalanceOf[ctx.token] -= collateralCount;
|
|
817
|
+
totalLoanedVestingAmountOf[ctx.hook][ctx.token] += collateralCount;
|
|
818
|
+
|
|
819
|
+
// Block same-position reentrancy before the loan contract burns collateral and returns the real loan ID.
|
|
820
|
+
activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token] = _PENDING_VESTING_LOAN_ID;
|
|
821
|
+
|
|
822
|
+
// Open the Revnet loan with this distributor as the holder whose tokens are burned as collateral.
|
|
823
|
+
loanId = _openVestingLoan({ctx: ctx, collateralCount: collateralCount});
|
|
824
|
+
if (loanId == 0 || loanId == _PENDING_VESTING_LOAN_ID) {
|
|
825
|
+
revert JBDistributor_InvalidVestingLoanId({loanId: loanId});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Track the distributor-held loan so repayment can restore the same vesting position.
|
|
829
|
+
activeVestingLoanIdOf[ctx.hook][ctx.tokenId][ctx.token] = loanId;
|
|
830
|
+
_vestingLoanOf[loanId] = JBVestingLoan({
|
|
831
|
+
hook: ctx.hook,
|
|
832
|
+
tokenId: ctx.tokenId,
|
|
833
|
+
token: ctx.token,
|
|
834
|
+
vestingDataCount: vestingDataCount,
|
|
835
|
+
collateralCount: collateralCount
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
_emitBorrowAgainstVesting({ctx: ctx, loanId: loanId, collateralCount: collateralCount});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/// @notice Emit the borrow event for a distributor-owned vesting loan.
|
|
842
|
+
/// @param ctx The borrow context.
|
|
843
|
+
/// @param loanId The Revnet loan NFT ID held by this distributor.
|
|
844
|
+
/// @param collateralCount The amount of vesting rewards used as collateral.
|
|
845
|
+
function _emitBorrowAgainstVesting(JBBorrowContext memory ctx, uint256 loanId, uint256 collateralCount) internal {
|
|
846
|
+
emit BorrowAgainstVesting({
|
|
847
|
+
hook: ctx.hook,
|
|
848
|
+
tokenId: ctx.tokenId,
|
|
849
|
+
token: ctx.token,
|
|
850
|
+
loanId: loanId,
|
|
851
|
+
revnetId: ctx.revnetId,
|
|
852
|
+
collateralCount: collateralCount,
|
|
853
|
+
sourceToken: ctx.sourceToken,
|
|
854
|
+
minBorrowAmount: ctx.minBorrowAmount,
|
|
855
|
+
prepaidFeePercent: ctx.prepaidFeePercent,
|
|
856
|
+
beneficiary: ctx.beneficiary,
|
|
857
|
+
caller: msg.sender
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/// @notice Open a Revnet loan against this distributor's vesting reward inventory.
|
|
862
|
+
/// @param ctx The borrow context.
|
|
863
|
+
/// @param collateralCount The amount of vesting rewards used as collateral.
|
|
864
|
+
/// @return loanId The Revnet loan NFT ID held by this distributor.
|
|
865
|
+
function _openVestingLoan(JBBorrowContext memory ctx, uint256 collateralCount) internal returns (uint256 loanId) {
|
|
866
|
+
(loanId,) = REV_LOANS.borrowFrom({
|
|
867
|
+
revnetId: ctx.revnetId,
|
|
868
|
+
token: ctx.sourceToken,
|
|
869
|
+
minBorrowAmount: ctx.minBorrowAmount,
|
|
870
|
+
collateralCount: collateralCount,
|
|
871
|
+
beneficiary: ctx.beneficiary,
|
|
872
|
+
prepaidFeePercent: ctx.prepaidFeePercent,
|
|
873
|
+
holder: address(this)
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/// @notice Repay a Revnet loan with the source token it borrowed.
|
|
878
|
+
/// @param loanId The Revnet loan NFT ID to repay.
|
|
879
|
+
/// @param loan The Revnet loan data.
|
|
880
|
+
/// @param repayBorrowAmount The amount of source token needed to repay the loan.
|
|
881
|
+
/// @param collateralCount The amount of collateral to return.
|
|
882
|
+
/// @return paidOffLoanId The paid-off loan ID returned by Revnet loans.
|
|
883
|
+
function _repayLoanSource(
|
|
884
|
+
uint256 loanId,
|
|
885
|
+
REVLoan memory loan,
|
|
886
|
+
uint256 repayBorrowAmount,
|
|
887
|
+
uint256 collateralCount
|
|
888
|
+
)
|
|
889
|
+
internal
|
|
890
|
+
returns (uint256 paidOffLoanId)
|
|
891
|
+
{
|
|
892
|
+
JBSingleAllowance memory allowance;
|
|
893
|
+
|
|
894
|
+
if (loan.sourceToken == JBConstants.NATIVE_TOKEN) {
|
|
895
|
+
// Native repayments must provide enough ETH for the exact current payoff.
|
|
896
|
+
if (msg.value < repayBorrowAmount) {
|
|
897
|
+
revert JBDistributor_InsufficientRepayAmount({amount: msg.value, requiredAmount: repayBorrowAmount});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Repay the loan and route returned collateral back to the distributor.
|
|
901
|
+
(paidOffLoanId,) = REV_LOANS.repayLoan{value: repayBorrowAmount}({
|
|
902
|
+
loanId: loanId,
|
|
903
|
+
maxRepayBorrowAmount: repayBorrowAmount,
|
|
904
|
+
collateralCountToReturn: collateralCount,
|
|
905
|
+
beneficiary: payable(address(this)),
|
|
906
|
+
allowance: allowance
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Return any native overpayment to the caller.
|
|
910
|
+
uint256 refundAmount = msg.value - repayBorrowAmount;
|
|
911
|
+
if (refundAmount != 0) {
|
|
912
|
+
(bool success,) = msg.sender.call{value: refundAmount}("");
|
|
913
|
+
if (!success) {
|
|
914
|
+
revert JBDistributor_NativeTransferFailed({beneficiary: msg.sender, amount: refundAmount});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
// ERC-20 repayments must not carry native ETH.
|
|
919
|
+
if (msg.value != 0) {
|
|
920
|
+
revert JBDistributor_UnexpectedNativeValue({msgValue: msg.value, token: loan.sourceToken});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Pull the exact current payoff from the caller.
|
|
924
|
+
IERC20 sourceToken = IERC20(loan.sourceToken);
|
|
925
|
+
sourceToken.safeTransferFrom({from: msg.sender, to: address(this), value: repayBorrowAmount});
|
|
926
|
+
|
|
927
|
+
// Approve only the exact amount needed for this repayment.
|
|
928
|
+
sourceToken.forceApprove({spender: address(REV_LOANS), value: repayBorrowAmount});
|
|
929
|
+
|
|
930
|
+
// Repay the loan and route returned collateral back to the distributor.
|
|
931
|
+
(paidOffLoanId,) = REV_LOANS.repayLoan({
|
|
932
|
+
loanId: loanId,
|
|
933
|
+
maxRepayBorrowAmount: repayBorrowAmount,
|
|
934
|
+
collateralCountToReturn: collateralCount,
|
|
935
|
+
beneficiary: payable(address(this)),
|
|
936
|
+
allowance: allowance
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Clear the temporary allowance for tokens that require explicit reset.
|
|
940
|
+
sourceToken.forceApprove({spender: address(REV_LOANS), value: 0});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/// @notice Restore repaid loan collateral to distributor inventory without changing vesting entries.
|
|
945
|
+
/// @param loanId The Revnet loan NFT ID that was repaid.
|
|
946
|
+
/// @param paidOffLoanId The paid-off loan ID returned by Revnet loans.
|
|
947
|
+
/// @param vestingLoan The vesting position that was collateralized.
|
|
948
|
+
/// @param rewardBalanceBefore The reward token balance before repayment.
|
|
949
|
+
/// @param repayBorrowAmount The amount repaid in the loan's source token.
|
|
950
|
+
function _restoreVestingCollateral(
|
|
951
|
+
uint256 loanId,
|
|
952
|
+
uint256 paidOffLoanId,
|
|
953
|
+
JBVestingLoan memory vestingLoan,
|
|
954
|
+
uint256 rewardBalanceBefore,
|
|
955
|
+
uint256 repayBorrowAmount
|
|
956
|
+
)
|
|
957
|
+
internal
|
|
958
|
+
{
|
|
959
|
+
// Measure the returned collateral and any same-token source-fee overflow.
|
|
960
|
+
uint256 rewardBalanceAfter = vestingLoan.token.balanceOf(address(this));
|
|
961
|
+
uint256 restoredAmount = rewardBalanceAfter > rewardBalanceBefore ? rewardBalanceAfter - rewardBalanceBefore : 0;
|
|
962
|
+
|
|
963
|
+
// Full repayment must return at least the collateral that was removed from inventory.
|
|
964
|
+
if (restoredAmount < vestingLoan.collateralCount) {
|
|
965
|
+
revert JBDistributor_InsufficientRepaidCollateral({
|
|
966
|
+
expectedAmount: vestingLoan.collateralCount, actualAmount: restoredAmount
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Put the collateral back into the hook's tracked inventory.
|
|
971
|
+
_balanceOf[vestingLoan.hook][vestingLoan.token] += vestingLoan.collateralCount;
|
|
972
|
+
_accountedBalanceOf[vestingLoan.token] += vestingLoan.collateralCount;
|
|
973
|
+
totalLoanedVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= vestingLoan.collateralCount;
|
|
974
|
+
|
|
975
|
+
// Clear the lock that prevented this position from being collected while collateralized.
|
|
976
|
+
delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
|
|
977
|
+
delete _vestingLoanOf[loanId];
|
|
978
|
+
|
|
979
|
+
// Return any excess reward tokens created during source-fee payment to the repayer.
|
|
980
|
+
uint256 excessRewardAmount = restoredAmount - vestingLoan.collateralCount;
|
|
981
|
+
if (excessRewardAmount != 0) {
|
|
982
|
+
vestingLoan.token.safeTransfer({to: msg.sender, value: excessRewardAmount});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
emit RepayVestingLoan({
|
|
986
|
+
loanId: loanId,
|
|
987
|
+
paidOffLoanId: paidOffLoanId,
|
|
988
|
+
token: vestingLoan.token,
|
|
989
|
+
collateralCount: vestingLoan.collateralCount,
|
|
990
|
+
repayBorrowAmount: repayBorrowAmount,
|
|
991
|
+
caller: msg.sender
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/// @notice Clear a stale vesting-loan lock after liquidation permanently destroys the collateral.
|
|
996
|
+
/// @param loanId The liquidated Revnet loan NFT ID.
|
|
997
|
+
/// @param vestingLoan The distributor-local vesting position that backed the loan.
|
|
998
|
+
/// @return collateralCount The amount of vesting rewards forfeited by liquidation.
|
|
999
|
+
function _writeOffLiquidatedVestingLoan(
|
|
1000
|
+
uint256 loanId,
|
|
1001
|
+
JBVestingLoan memory vestingLoan
|
|
1002
|
+
)
|
|
1003
|
+
internal
|
|
1004
|
+
returns (uint256 collateralCount)
|
|
1005
|
+
{
|
|
1006
|
+
// Cache the collateral amount because it is used for accounting and the event.
|
|
1007
|
+
collateralCount = vestingLoan.collateralCount;
|
|
1008
|
+
|
|
1009
|
+
// Load the vesting entries for the token ID whose rewards were collateralized.
|
|
1010
|
+
JBVestingData[] storage vestings = vestingDataOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
|
|
1011
|
+
|
|
1012
|
+
// Start at the first unexhausted vesting entry.
|
|
1013
|
+
uint256 vestedIndex = latestVestedIndexOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
|
|
1014
|
+
|
|
1015
|
+
// Stop at the boundary recorded when the loan opened, preserving newer vesting entries.
|
|
1016
|
+
uint256 vestingDataCount = vestingLoan.vestingDataCount;
|
|
1017
|
+
|
|
1018
|
+
// Mark each collateralized entry fully claimed because Revnet liquidation destroyed its backing tokens.
|
|
1019
|
+
while (vestedIndex < vestingDataCount) {
|
|
1020
|
+
vestings[vestedIndex].shareClaimed = MAX_SHARE;
|
|
1021
|
+
|
|
1022
|
+
unchecked {
|
|
1023
|
+
// Safe because the loop is bounded by the recorded vesting-entry count.
|
|
1024
|
+
++vestedIndex;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Skip over the written-off vesting entries without ever moving the cursor backwards.
|
|
1029
|
+
latestVestedIndexOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token] = vestedIndex;
|
|
1030
|
+
|
|
1031
|
+
// Remove the liquidated collateral from the amount still considered vesting.
|
|
1032
|
+
totalVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= collateralCount;
|
|
1033
|
+
|
|
1034
|
+
// Remove the liquidated collateral from the loaned-vesting inventory.
|
|
1035
|
+
totalLoanedVestingAmountOf[vestingLoan.hook][vestingLoan.token] -= collateralCount;
|
|
1036
|
+
|
|
1037
|
+
// Clear the active loan lock for this token ID and reward token.
|
|
1038
|
+
delete activeVestingLoanIdOf[vestingLoan.hook][vestingLoan.tokenId][vestingLoan.token];
|
|
1039
|
+
|
|
1040
|
+
// Clear the loan metadata so it cannot be written off or repaid again.
|
|
1041
|
+
delete _vestingLoanOf[loanId];
|
|
1042
|
+
|
|
1043
|
+
emit LiquidatedVestingLoanWrittenOff({
|
|
1044
|
+
hook: vestingLoan.hook,
|
|
1045
|
+
tokenId: vestingLoan.tokenId,
|
|
1046
|
+
token: vestingLoan.token,
|
|
1047
|
+
loanId: loanId,
|
|
1048
|
+
collateralCount: collateralCount,
|
|
1049
|
+
caller: msg.sender
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
552
1053
|
/// @notice Accepts an ERC-20 funding transfer and returns the actual balance delta.
|
|
553
1054
|
/// @param token The ERC-20 token to accept.
|
|
554
1055
|
/// @param from The address to pull tokens from.
|
|
@@ -587,8 +1088,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
587
1088
|
/// @param hook The stake source whose stakers receive the rewards.
|
|
588
1089
|
/// @param token The reward token being funded.
|
|
589
1090
|
/// @param amount The nominal amount to fund.
|
|
590
|
-
|
|
591
|
-
function _fund(address hook, IERC20 token, uint256 amount, uint48 claimDuration) internal {
|
|
1091
|
+
function _fund(address hook, IERC20 token, uint256 amount) internal {
|
|
592
1092
|
// Native funding is measured by msg.value, not the caller-provided amount.
|
|
593
1093
|
if (address(token) == JBConstants.NATIVE_TOKEN) {
|
|
594
1094
|
amount = msg.value;
|
|
@@ -603,28 +1103,43 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
603
1103
|
}
|
|
604
1104
|
|
|
605
1105
|
// Store the accepted amount in this round's historical reward ledger.
|
|
606
|
-
_recordRewardFunding({hook: hook, token: token, amount: amount
|
|
1106
|
+
_recordRewardFunding({hook: hook, token: token, amount: amount});
|
|
607
1107
|
}
|
|
608
1108
|
|
|
609
1109
|
/// @notice Record accepted funding as the current round's reward pot.
|
|
610
1110
|
/// @param hook The stake source whose stakers receive the rewards.
|
|
611
1111
|
/// @param token The reward token.
|
|
612
1112
|
/// @param amount The accepted funding amount.
|
|
613
|
-
|
|
614
|
-
function _recordRewardFunding(address hook, IERC20 token, uint256 amount, uint48 claimDuration) internal {
|
|
1113
|
+
function _recordRewardFunding(address hook, IERC20 token, uint256 amount) internal {
|
|
615
1114
|
// Zero-value transfers do not create reward rounds or alter tracked balances.
|
|
616
1115
|
if (amount == 0) return;
|
|
617
1116
|
|
|
618
|
-
//
|
|
1117
|
+
// Add the accepted amount to the current reward ledger.
|
|
1118
|
+
_recordRewardRound({hook: hook, token: token, amount: amount});
|
|
1119
|
+
|
|
1120
|
+
// Keep the base distributor's balance accounting in sync for collection and conservation checks.
|
|
1121
|
+
_balanceOf[hook][token] += amount;
|
|
1122
|
+
_accountedBalanceOf[token] += amount;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/// @notice Record rewards as the current round's claimable historical reward pot.
|
|
1126
|
+
/// @param hook The stake source whose stakers receive the rewards.
|
|
1127
|
+
/// @param token The reward token.
|
|
1128
|
+
/// @param amount The amount to add to the current reward round.
|
|
1129
|
+
function _recordRewardRound(address hook, IERC20 token, uint256 amount) internal {
|
|
1130
|
+
// Zero-value rewards do not create reward rounds.
|
|
1131
|
+
if (amount == 0) return;
|
|
1132
|
+
|
|
1133
|
+
// Rewards belong to the round in progress when they enter the ledger.
|
|
619
1134
|
uint256 round = currentRound();
|
|
620
1135
|
|
|
621
1136
|
// Load the current round's ledger entry for this hook and reward token.
|
|
622
1137
|
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
|
|
623
1138
|
|
|
624
|
-
//
|
|
625
|
-
uint48 claimDeadline = _claimDeadlineFor(
|
|
1139
|
+
// Every reward round in this contract uses the same immutable claim duration.
|
|
1140
|
+
uint48 claimDeadline = _claimDeadlineFor(round);
|
|
626
1141
|
|
|
627
|
-
// First
|
|
1142
|
+
// First value in a round locks that round's snapshot block and total stake.
|
|
628
1143
|
if (rewardRound.amount == 0) {
|
|
629
1144
|
// Record the exact historical block used for all stake lookups in this round.
|
|
630
1145
|
uint256 snapshotBlock = _ensureSnapshotBlockFor(round);
|
|
@@ -632,24 +1147,15 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
632
1147
|
// Store the snapshot block in the packed uint48 field.
|
|
633
1148
|
rewardRound.snapshotBlock = _toUint48(snapshotBlock);
|
|
634
1149
|
|
|
635
|
-
// Store the packed claim deadline
|
|
1150
|
+
// Store the packed claim deadline fixed for this distributor.
|
|
636
1151
|
rewardRound.claimDeadline = claimDeadline;
|
|
637
1152
|
|
|
638
1153
|
// Store the packed total stake that shares this round's reward pot.
|
|
639
1154
|
rewardRound.totalStake = _toUint208(_totalStake({hook: hook, blockNumber: snapshotBlock}));
|
|
640
|
-
} else if (rewardRound.claimDeadline != claimDeadline) {
|
|
641
|
-
// All fundings merged into the same round must have one deadline for deterministic expiry.
|
|
642
|
-
revert JBDistributor_ClaimDeadlineMismatch({
|
|
643
|
-
existingDeadline: rewardRound.claimDeadline, newDeadline: claimDeadline
|
|
644
|
-
});
|
|
645
1155
|
}
|
|
646
1156
|
|
|
647
|
-
// Multiple
|
|
1157
|
+
// Multiple additions in the same round share the same snapshot and reward pot.
|
|
648
1158
|
rewardRound.amount = _toUint208(uint256(rewardRound.amount) + amount);
|
|
649
|
-
|
|
650
|
-
// Keep the base distributor's balance accounting in sync for collection and conservation checks.
|
|
651
|
-
_balanceOf[hook][token] += amount;
|
|
652
|
-
_accountedBalanceOf[token] += amount;
|
|
653
1159
|
}
|
|
654
1160
|
|
|
655
1161
|
/// @notice Burn one expired reward round's unclaimed inventory.
|
|
@@ -673,14 +1179,14 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
673
1179
|
// Mark the whole round settled before transferring to close reentrancy-sensitive accounting.
|
|
674
1180
|
rewardRound.claimedAmount = rewardRound.amount;
|
|
675
1181
|
|
|
676
|
-
// Remove the expired remainder from distributor inventory and
|
|
1182
|
+
// Remove the expired remainder from distributor inventory and burn it through the JB controller.
|
|
677
1183
|
_burnRewardTokens({hook: hook, token: token, amount: burnAmount});
|
|
678
1184
|
|
|
679
1185
|
// Surface the permissionless burn for off-chain accounting.
|
|
680
1186
|
emit ExpiredRewardsBurned({hook: hook, round: round, token: token, amount: burnAmount, caller: msg.sender});
|
|
681
1187
|
}
|
|
682
1188
|
|
|
683
|
-
/// @notice Burn reward inventory
|
|
1189
|
+
/// @notice Burn reward inventory using the JB controller.
|
|
684
1190
|
/// @param hook The hook whose tracked balance is being burned.
|
|
685
1191
|
/// @param token The reward token to burn.
|
|
686
1192
|
/// @param amount The amount to burn.
|
|
@@ -688,22 +1194,35 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
688
1194
|
// No-op zero burns so callers can batch empty or already-settled rounds safely.
|
|
689
1195
|
if (amount == 0) return;
|
|
690
1196
|
|
|
1197
|
+
// A missing controller means there is no burn authority for any reward token.
|
|
1198
|
+
if (address(CONTROLLER) == address(0)) revert JBDistributor_TokenNotBurnable({token: address(token)});
|
|
1199
|
+
|
|
1200
|
+
// Only JB project tokens can be burned through `JBController.burnTokensOf`.
|
|
1201
|
+
uint256 projectId = CONTROLLER.TOKENS().projectIdOf({token: IJBToken(address(token))});
|
|
1202
|
+
|
|
1203
|
+
// Revert instead of sending unsupported rewards to a burn address.
|
|
1204
|
+
if (projectId == 0) revert JBDistributor_TokenNotBurnable({token: address(token)});
|
|
1205
|
+
|
|
691
1206
|
// Remove the burned amount from the hook's reward inventory.
|
|
692
1207
|
_balanceOf[hook][token] -= amount;
|
|
693
1208
|
|
|
694
1209
|
// Remove the same amount from the global inventory tracked for this token.
|
|
695
1210
|
_accountedBalanceOf[token] -= amount;
|
|
696
1211
|
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
(bool success,) = BURN_ADDRESS.call{value: amount}("");
|
|
1212
|
+
// Burn from this distributor's project-token balance or token credits.
|
|
1213
|
+
CONTROLLER.burnTokensOf({holder: address(this), projectId: projectId, tokenCount: amount, memo: ""});
|
|
1214
|
+
}
|
|
701
1215
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1216
|
+
/// @notice Resolve the revnet project ID for a reward token.
|
|
1217
|
+
/// @param token The reward token to resolve.
|
|
1218
|
+
/// @return revnetId The token's revnet project ID.
|
|
1219
|
+
function _revnetIdOf(IERC20 token) internal view returns (uint256 revnetId) {
|
|
1220
|
+
// The reward token must be registered as a JB project token.
|
|
1221
|
+
revnetId = CONTROLLER.TOKENS().projectIdOf({token: IJBToken(address(token))});
|
|
1222
|
+
|
|
1223
|
+
// The project must be owned by the configured REVOwner.
|
|
1224
|
+
if (revnetId == 0 || CONTROLLER.PROJECTS().ownerOf(revnetId) != address(REV_OWNER)) {
|
|
1225
|
+
revert JBDistributor_NotRevnetRewardToken({token: address(token)});
|
|
707
1226
|
}
|
|
708
1227
|
}
|
|
709
1228
|
|
|
@@ -716,7 +1235,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
716
1235
|
castValue = uint208(value);
|
|
717
1236
|
}
|
|
718
1237
|
|
|
719
|
-
/// @notice Cast a
|
|
1238
|
+
/// @notice Cast a value to uint48.
|
|
720
1239
|
/// @param value The value to cast.
|
|
721
1240
|
/// @return castValue The cast value.
|
|
722
1241
|
function _toUint48(uint256 value) internal pure returns (uint48 castValue) {
|
|
@@ -763,9 +1282,11 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
763
1282
|
return _snapshotAtRoundOf[hook][token][round];
|
|
764
1283
|
}
|
|
765
1284
|
|
|
1285
|
+
// Exclude collateralized vesting inventory because those tokens have been burned into distributor-held loans.
|
|
1286
|
+
uint256 vestingAmount = totalVestingAmountOf[hook][token] - totalLoanedVestingAmountOf[hook][token];
|
|
1287
|
+
|
|
766
1288
|
// Take a snapshot using the hook's tracked balance.
|
|
767
|
-
snapshot =
|
|
768
|
-
JBTokenSnapshotData({balance: _balanceOf[hook][token], vestingAmount: totalVestingAmountOf[hook][token]});
|
|
1289
|
+
snapshot = JBTokenSnapshotData({balance: _balanceOf[hook][token], vestingAmount: vestingAmount});
|
|
769
1290
|
|
|
770
1291
|
// Store the snapshot and mark it initialized.
|
|
771
1292
|
_snapshotAtRoundOf[hook][token][round] = snapshot;
|
|
@@ -781,16 +1302,15 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
781
1302
|
});
|
|
782
1303
|
}
|
|
783
1304
|
|
|
784
|
-
/// @notice The deadline for a reward round
|
|
1305
|
+
/// @notice The deadline for a reward round using this distributor's immutable claim duration.
|
|
785
1306
|
/// @param round The reward round.
|
|
786
|
-
/// @param claimDuration The claim duration once the round becomes claimable.
|
|
787
1307
|
/// @return claimDeadline The deadline timestamp. Zero means no expiration.
|
|
788
|
-
function _claimDeadlineFor(uint256 round
|
|
1308
|
+
function _claimDeadlineFor(uint256 round) internal view returns (uint48 claimDeadline) {
|
|
789
1309
|
// Zero duration keeps the round non-expiring and backward compatible with existing fund paths.
|
|
790
|
-
if (
|
|
1310
|
+
if (CLAIM_DURATION == 0) return 0;
|
|
791
1311
|
|
|
792
1312
|
// Start the window at the next round boundary, when the funded round first becomes claimable.
|
|
793
|
-
claimDeadline = _toUint48(roundStartTimestamp(round + 1) +
|
|
1313
|
+
claimDeadline = _toUint48(roundStartTimestamp(round + 1) + CLAIM_DURATION);
|
|
794
1314
|
}
|
|
795
1315
|
|
|
796
1316
|
/// @notice Whether a reward round has passed its claim deadline.
|
|
@@ -852,9 +1372,10 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
852
1372
|
} else {
|
|
853
1373
|
token.safeTransfer({to: beneficiary, value: totalTokenAmount});
|
|
854
1374
|
}
|
|
1375
|
+
} else {
|
|
1376
|
+
// If forfeiture: remove the unlocked amount from inventory and burn it through the JB controller.
|
|
1377
|
+
_burnRewardTokens({hook: hook, token: token, amount: totalTokenAmount});
|
|
855
1378
|
}
|
|
856
|
-
// If forfeiture: _balanceOf is NOT decremented so the forfeited tokens
|
|
857
|
-
// return to the hook's distributable pool for future rounds.
|
|
858
1379
|
}
|
|
859
1380
|
|
|
860
1381
|
unchecked {
|
|
@@ -881,6 +1402,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
881
1402
|
for (uint256 j; j < tokenIds.length;) {
|
|
882
1403
|
uint256 tokenId = tokenIds[j];
|
|
883
1404
|
|
|
1405
|
+
// Loan collateral stays locked until repayment restores it to this distributor.
|
|
1406
|
+
_requireNoActiveVestingLoan({hook: hook, tokenId: tokenId, token: token});
|
|
1407
|
+
|
|
884
1408
|
// Keep a reference to the latest vested index.
|
|
885
1409
|
uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
|
|
886
1410
|
|
|
@@ -898,7 +1422,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
898
1422
|
uint256 lockedShare = JBVestingMath.lockedShareOf({
|
|
899
1423
|
releaseRound: vesting.releaseRound,
|
|
900
1424
|
currentRound: round,
|
|
901
|
-
vestingRounds:
|
|
1425
|
+
vestingRounds: VESTING_ROUNDS,
|
|
902
1426
|
maxShare: MAX_SHARE
|
|
903
1427
|
});
|
|
904
1428
|
|
|
@@ -1029,6 +1553,41 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1029
1553
|
// ----------------------- internal views ---------------------------- //
|
|
1030
1554
|
//*********************************************************************//
|
|
1031
1555
|
|
|
1556
|
+
/// @notice The remaining uncollected vesting amount for one token ID and reward token.
|
|
1557
|
+
/// @param hook The hook the token ID belongs to.
|
|
1558
|
+
/// @param tokenId The token ID to check.
|
|
1559
|
+
/// @param token The reward token to check.
|
|
1560
|
+
/// @return tokenAmount The amount still locked or unlocked-but-uncollected.
|
|
1561
|
+
function _unclaimedVestingAmountOf(
|
|
1562
|
+
address hook,
|
|
1563
|
+
uint256 tokenId,
|
|
1564
|
+
IERC20 token
|
|
1565
|
+
)
|
|
1566
|
+
internal
|
|
1567
|
+
view
|
|
1568
|
+
returns (uint256 tokenAmount)
|
|
1569
|
+
{
|
|
1570
|
+
// Keep a reference to the latest fully vested index.
|
|
1571
|
+
uint256 vestedIndex = latestVestedIndexOf[hook][tokenId][token];
|
|
1572
|
+
|
|
1573
|
+
// Keep a reference to the number of vesting entries for the token ID and token.
|
|
1574
|
+
uint256 numberOfVestingRounds = vestingDataOf[hook][tokenId][token].length;
|
|
1575
|
+
|
|
1576
|
+
while (vestedIndex < numberOfVestingRounds) {
|
|
1577
|
+
// Keep a reference to the vested data being iterated on.
|
|
1578
|
+
JBVestingData memory vesting = vestingDataOf[hook][tokenId][token][vestedIndex];
|
|
1579
|
+
|
|
1580
|
+
// Use `original - alreadyPaid` to include rounding dust in the remaining amount.
|
|
1581
|
+
tokenAmount += JBVestingMath.unclaimedAmountOf({
|
|
1582
|
+
amount: vesting.amount, shareClaimed: vesting.shareClaimed, maxShare: MAX_SHARE
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
unchecked {
|
|
1586
|
+
++vestedIndex;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1032
1591
|
/// @notice Check whether an account is authorized to collect vested rewards for the given token ID. For 721
|
|
1033
1592
|
/// distributors this is ownership; for token distributors this is address-encoding match.
|
|
1034
1593
|
/// @param hook The hook the token belongs to.
|
|
@@ -1045,6 +1604,19 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1045
1604
|
if (token != address(0)) revert JBDistributor_ReentrantTokenTransfer(token);
|
|
1046
1605
|
}
|
|
1047
1606
|
|
|
1607
|
+
/// @notice Revert if a token ID's vesting rewards are locked in a distributor-owned loan.
|
|
1608
|
+
/// @param hook The hook the token ID belongs to.
|
|
1609
|
+
/// @param tokenId The token ID to check.
|
|
1610
|
+
/// @param token The reward token to check.
|
|
1611
|
+
function _requireNoActiveVestingLoan(address hook, uint256 tokenId, IERC20 token) internal view {
|
|
1612
|
+
uint256 loanId = activeVestingLoanIdOf[hook][tokenId][token];
|
|
1613
|
+
if (loanId != 0) {
|
|
1614
|
+
revert JBDistributor_VestingLoanOutstanding({
|
|
1615
|
+
hook: hook, tokenId: tokenId, token: address(token), loanId: loanId
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1048
1620
|
/// @notice Check whether a staker token has been burned. Burned tokens are excluded from stake calculations
|
|
1049
1621
|
/// and their unvested rewards can be released via `releaseForfeitedRewards`.
|
|
1050
1622
|
/// @param hook The hook the token belongs to.
|