@bananapus/distributor-v6 0.0.30 → 0.0.31
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/package.json
CHANGED
package/src/JB721Distributor.sol
CHANGED
|
@@ -85,7 +85,7 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
85
85
|
//*********************************************************************//
|
|
86
86
|
|
|
87
87
|
/// @param directory The JB directory used to verify terminal/controller callers.
|
|
88
|
-
/// @param controller The JB controller used to burn
|
|
88
|
+
/// @param controller The JB controller used to burn forfeited project-token rewards.
|
|
89
89
|
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
90
90
|
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
91
91
|
/// @param initialRoundDuration The duration of each round, specified in seconds.
|
|
@@ -295,9 +295,9 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
295
295
|
|
|
296
296
|
// Skip rounds that never received funding.
|
|
297
297
|
if (rewardRound.amount != 0) {
|
|
298
|
-
// Expired rounds can no longer be claimed;
|
|
298
|
+
// Expired rounds can no longer be claimed as-is; recycle their unclaimed remainder instead.
|
|
299
299
|
if (_rewardRoundExpired(rewardRound)) {
|
|
300
|
-
|
|
300
|
+
_recycleExpiredRewardRound({hook: ctx.hook, token: token, round: rewardRoundNumber});
|
|
301
301
|
} else if (rewardRound.totalStake != 0) {
|
|
302
302
|
// Bundle the fixed round data used by every NFT in the batch.
|
|
303
303
|
JBVestContext memory vestCtx = JBVestContext({
|
|
@@ -314,7 +314,8 @@ contract JB721Distributor is JBDistributor, IJB721Distributor {
|
|
|
314
314
|
uint256 roundVestingAmount =
|
|
315
315
|
_claimRewardRoundForTokenIds({ctx: vestCtx, tokenIds: tokenIds, tokenAmounts: tokenAmounts});
|
|
316
316
|
|
|
317
|
-
// Track only the amount that actually started vesting, leaving zero-vote and dust amounts
|
|
317
|
+
// Track only the amount that actually started vesting, leaving zero-vote and dust amounts
|
|
318
|
+
// recyclable.
|
|
318
319
|
if (roundVestingAmount != 0) {
|
|
319
320
|
rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + roundVestingAmount);
|
|
320
321
|
|
package/src/JBDistributor.sol
CHANGED
|
@@ -80,6 +80,9 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
80
80
|
/// @notice Thrown when unexpected native ETH is sent with an ERC-20 operation.
|
|
81
81
|
error JBDistributor_UnexpectedNativeValue(uint256 msgValue, address token);
|
|
82
82
|
|
|
83
|
+
/// @notice Thrown when an ERC-20 repayment does not credit the exact amount pulled from the caller.
|
|
84
|
+
error JBDistributor_UnexpectedRepayAmount(uint256 amount, uint256 expectedAmount);
|
|
85
|
+
|
|
83
86
|
/// @notice Thrown when a function requires exactly one reward token.
|
|
84
87
|
error JBDistributor_UnexpectedTokenCount(uint256 tokenCount);
|
|
85
88
|
|
|
@@ -123,7 +126,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
123
126
|
/// @dev A zero duration means reward rounds do not expire.
|
|
124
127
|
uint48 public immutable override CLAIM_DURATION;
|
|
125
128
|
|
|
126
|
-
/// @notice The JB controller used to burn
|
|
129
|
+
/// @notice The JB controller used to burn forfeited project-token rewards.
|
|
127
130
|
IJBController public immutable override CONTROLLER;
|
|
128
131
|
|
|
129
132
|
/// @notice The duration of each round, specified in seconds.
|
|
@@ -228,7 +231,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
228
231
|
// -------------------------- constructor ---------------------------- //
|
|
229
232
|
//*********************************************************************//
|
|
230
233
|
|
|
231
|
-
/// @param controller The JB controller used to burn
|
|
234
|
+
/// @param controller The JB controller used to burn forfeited project-token rewards.
|
|
232
235
|
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
233
236
|
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
234
237
|
/// @param initialRoundDuration The duration of each round, specified in seconds.
|
|
@@ -349,11 +352,12 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
349
352
|
_fund({hook: hook, token: token, amount: amount});
|
|
350
353
|
}
|
|
351
354
|
|
|
352
|
-
/// @notice
|
|
353
|
-
/// @
|
|
354
|
-
/// @param
|
|
355
|
-
/// @param
|
|
356
|
-
/// @
|
|
355
|
+
/// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
|
|
356
|
+
/// @dev The selector name is kept for compatibility with existing keeper integrations.
|
|
357
|
+
/// @param hook The hook whose expired rewards should be recycled.
|
|
358
|
+
/// @param token The reward token to recycle.
|
|
359
|
+
/// @param rounds The reward rounds to recycle.
|
|
360
|
+
/// @return amount The total amount recycled.
|
|
357
361
|
function burnExpiredRewards(
|
|
358
362
|
address hook,
|
|
359
363
|
IERC20 token,
|
|
@@ -364,13 +368,13 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
364
368
|
override
|
|
365
369
|
returns (uint256 amount)
|
|
366
370
|
{
|
|
367
|
-
// Do not let reward-token callbacks
|
|
371
|
+
// Do not let reward-token callbacks recycle inventory during an inbound balance-delta measurement.
|
|
368
372
|
_requireNotAcceptingToken();
|
|
369
373
|
|
|
370
374
|
// Process every requested round independently so callers can batch keeper work.
|
|
371
375
|
for (uint256 i; i < rounds.length;) {
|
|
372
376
|
// Add this round's expired remainder to the batch total.
|
|
373
|
-
amount +=
|
|
377
|
+
amount += _recycleExpiredRewardRound({hook: hook, token: token, round: rounds[i]});
|
|
374
378
|
|
|
375
379
|
unchecked {
|
|
376
380
|
// Safe because the loop is bounded by calldata length.
|
|
@@ -920,9 +924,14 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
920
924
|
revert JBDistributor_UnexpectedNativeValue({msgValue: msg.value, token: loan.sourceToken});
|
|
921
925
|
}
|
|
922
926
|
|
|
923
|
-
// Pull the exact current payoff from the caller.
|
|
927
|
+
// Pull the exact current payoff from the caller. Existing distributor inventory must not cover a shortfall.
|
|
924
928
|
IERC20 sourceToken = IERC20(loan.sourceToken);
|
|
929
|
+
uint256 sourceBalanceBefore = sourceToken.balanceOf(address(this));
|
|
925
930
|
sourceToken.safeTransferFrom({from: msg.sender, to: address(this), value: repayBorrowAmount});
|
|
931
|
+
uint256 receivedAmount = sourceToken.balanceOf(address(this)) - sourceBalanceBefore;
|
|
932
|
+
if (receivedAmount != repayBorrowAmount) {
|
|
933
|
+
revert JBDistributor_UnexpectedRepayAmount({amount: receivedAmount, expectedAmount: repayBorrowAmount});
|
|
934
|
+
}
|
|
926
935
|
|
|
927
936
|
// Approve only the exact amount needed for this repayment.
|
|
928
937
|
sourceToken.forceApprove({spender: address(REV_LOANS), value: repayBorrowAmount});
|
|
@@ -1158,32 +1167,47 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1158
1167
|
rewardRound.amount = _toUint208(uint256(rewardRound.amount) + amount);
|
|
1159
1168
|
}
|
|
1160
1169
|
|
|
1161
|
-
/// @notice
|
|
1162
|
-
/// @param hook The hook whose expired rewards should be
|
|
1163
|
-
/// @param token The reward token to
|
|
1164
|
-
/// @param round The reward round to
|
|
1165
|
-
/// @return
|
|
1166
|
-
function
|
|
1170
|
+
/// @notice Recycle one expired reward round's unclaimed inventory into the current reward round.
|
|
1171
|
+
/// @param hook The hook whose expired rewards should be recycled.
|
|
1172
|
+
/// @param token The reward token to recycle.
|
|
1173
|
+
/// @param round The reward round to recycle.
|
|
1174
|
+
/// @return recycleAmount The amount recycled.
|
|
1175
|
+
function _recycleExpiredRewardRound(
|
|
1176
|
+
address hook,
|
|
1177
|
+
IERC20 token,
|
|
1178
|
+
uint256 round
|
|
1179
|
+
)
|
|
1180
|
+
internal
|
|
1181
|
+
returns (uint256 recycleAmount)
|
|
1182
|
+
{
|
|
1167
1183
|
// Load the reward round once so expiry, claimed amount, and funded amount stay in sync.
|
|
1168
1184
|
JBRewardRoundData storage rewardRound = rewardRoundOf[hook][token][round];
|
|
1169
1185
|
|
|
1170
1186
|
// Ignore rounds that either never expire or have not reached their deadline yet.
|
|
1171
1187
|
if (!_rewardRoundExpired(rewardRound)) return 0;
|
|
1172
1188
|
|
|
1173
|
-
// If prior claims have already materialized the whole round, there is nothing left to
|
|
1189
|
+
// If prior claims have already materialized the whole round, there is nothing left to recycle.
|
|
1174
1190
|
if (rewardRound.claimedAmount >= rewardRound.amount) return 0;
|
|
1175
1191
|
|
|
1176
|
-
//
|
|
1177
|
-
|
|
1192
|
+
// Recycle only the unclaimed remainder, preserving amounts that already started vesting.
|
|
1193
|
+
recycleAmount = uint256(rewardRound.amount) - uint256(rewardRound.claimedAmount);
|
|
1178
1194
|
|
|
1179
|
-
// Mark the whole round settled before
|
|
1195
|
+
// Mark the whole round settled before writing the recycled amount into a fresh round.
|
|
1180
1196
|
rewardRound.claimedAmount = rewardRound.amount;
|
|
1181
1197
|
|
|
1182
|
-
//
|
|
1183
|
-
|
|
1198
|
+
// Keep the inventory in the distributor and give the current staker set a new claimable round.
|
|
1199
|
+
uint256 recycledToRound = currentRound();
|
|
1200
|
+
_recordRewardRound({hook: hook, token: token, amount: recycleAmount});
|
|
1184
1201
|
|
|
1185
|
-
// Surface the permissionless
|
|
1186
|
-
emit
|
|
1202
|
+
// Surface the permissionless recycle for off-chain accounting.
|
|
1203
|
+
emit ExpiredRewardsRecycled({
|
|
1204
|
+
hook: hook,
|
|
1205
|
+
fromRound: round,
|
|
1206
|
+
toRound: recycledToRound,
|
|
1207
|
+
token: token,
|
|
1208
|
+
amount: recycleAmount,
|
|
1209
|
+
caller: msg.sender
|
|
1210
|
+
});
|
|
1187
1211
|
}
|
|
1188
1212
|
|
|
1189
1213
|
/// @notice Burn reward inventory using the JB controller.
|
|
@@ -1315,7 +1339,7 @@ abstract contract JBDistributor is IJBDistributor {
|
|
|
1315
1339
|
|
|
1316
1340
|
/// @notice Whether a reward round has passed its claim deadline.
|
|
1317
1341
|
/// @param rewardRound The reward round data.
|
|
1318
|
-
/// @return expired True if unclaimed rewards can be
|
|
1342
|
+
/// @return expired True if unclaimed rewards can be recycled.
|
|
1319
1343
|
function _rewardRoundExpired(JBRewardRoundData storage rewardRound) internal view returns (bool expired) {
|
|
1320
1344
|
// Copy the packed deadline into memory so the zero check and timestamp compare use the same value.
|
|
1321
1345
|
uint48 claimDeadline = rewardRound.claimDeadline;
|
|
@@ -69,7 +69,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
69
69
|
//*********************************************************************//
|
|
70
70
|
|
|
71
71
|
/// @param directory The JB directory used to verify terminal/controller callers.
|
|
72
|
-
/// @param controller The JB controller used to burn
|
|
72
|
+
/// @param controller The JB controller used to burn forfeited project-token rewards.
|
|
73
73
|
/// @param revLoans The Revnet loans contract used to borrow against vested revnet rewards.
|
|
74
74
|
/// @param revOwner The REVOwner contract that must own revnet reward token projects.
|
|
75
75
|
/// @param initialRoundDuration The duration of each round, specified in seconds.
|
|
@@ -328,9 +328,9 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
328
328
|
|
|
329
329
|
// Skip rounds that never received funding.
|
|
330
330
|
if (rewardRound.amount != 0) {
|
|
331
|
-
// Expired rounds can no longer be claimed;
|
|
331
|
+
// Expired rounds can no longer be claimed as-is; recycle their unclaimed remainder instead.
|
|
332
332
|
if (_rewardRoundExpired(rewardRound)) {
|
|
333
|
-
|
|
333
|
+
_recycleExpiredRewardRound({hook: hook, token: token, round: rewardRoundNumber});
|
|
334
334
|
} else if (rewardRound.totalStake != 0) {
|
|
335
335
|
// Use the funding round's snapshot block, not the block at which the staker finally claims.
|
|
336
336
|
uint256 tokenStakeAmount =
|
|
@@ -344,7 +344,7 @@ contract JBTokenDistributor is JBDistributor, IJBTokenDistributor {
|
|
|
344
344
|
|
|
345
345
|
// Ignore floor-rounded zero claims to avoid unnecessary storage writes.
|
|
346
346
|
if (claimAmount != 0) {
|
|
347
|
-
// Track the portion that has started vesting so expiry
|
|
347
|
+
// Track the portion that has started vesting so expiry recycles only the remainder.
|
|
348
348
|
rewardRound.claimedAmount = _toUint208(uint256(rewardRound.claimedAmount) + claimAmount);
|
|
349
349
|
|
|
350
350
|
// Add this round's vested amount to the staker's cumulative claim.
|
|
@@ -81,14 +81,20 @@ interface IJBDistributor {
|
|
|
81
81
|
/// @param caller The address that triggered the snapshot recording.
|
|
82
82
|
event RoundSnapshotRecorded(uint256 indexed round, uint256 snapshotBlock, address caller);
|
|
83
83
|
|
|
84
|
-
/// @notice Emitted when an expired reward round's unclaimed amount is
|
|
85
|
-
/// @param hook The hook whose expired rewards were
|
|
86
|
-
/// @param
|
|
87
|
-
/// @param
|
|
88
|
-
/// @param
|
|
89
|
-
/// @param
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
/// @notice Emitted when an expired reward round's unclaimed amount is recycled into a later reward round.
|
|
85
|
+
/// @param hook The hook whose expired rewards were recycled.
|
|
86
|
+
/// @param fromRound The expired reward round.
|
|
87
|
+
/// @param toRound The reward round receiving the recycled rewards.
|
|
88
|
+
/// @param token The reward token that was recycled.
|
|
89
|
+
/// @param amount The unclaimed reward amount recycled.
|
|
90
|
+
/// @param caller The address that triggered the recycle.
|
|
91
|
+
event ExpiredRewardsRecycled(
|
|
92
|
+
address indexed hook,
|
|
93
|
+
uint256 indexed fromRound,
|
|
94
|
+
uint256 indexed toRound,
|
|
95
|
+
IERC20 token,
|
|
96
|
+
uint256 amount,
|
|
97
|
+
address caller
|
|
92
98
|
);
|
|
93
99
|
|
|
94
100
|
/// @notice Emitted when a liquidated distributor-held Revnet loan is written off.
|
|
@@ -147,7 +153,7 @@ interface IJBDistributor {
|
|
|
147
153
|
/// @dev A zero duration means reward rounds do not expire.
|
|
148
154
|
function CLAIM_DURATION() external view returns (uint48);
|
|
149
155
|
|
|
150
|
-
/// @notice The JB controller used to burn
|
|
156
|
+
/// @notice The JB controller used to burn forfeited project-token rewards.
|
|
151
157
|
function CONTROLLER() external view returns (IJBController);
|
|
152
158
|
|
|
153
159
|
/// @notice The duration of each round, specified in seconds.
|
|
@@ -279,11 +285,11 @@ interface IJBDistributor {
|
|
|
279
285
|
/// @param amount The amount to fund.
|
|
280
286
|
function fund(address hook, IERC20 token, uint256 amount) external payable;
|
|
281
287
|
|
|
282
|
-
/// @notice
|
|
283
|
-
/// @param hook The hook whose expired reward rounds should be
|
|
284
|
-
/// @param token The reward token to
|
|
285
|
-
/// @param rounds The reward rounds to
|
|
286
|
-
/// @return amount The total amount
|
|
288
|
+
/// @notice Recycle unclaimed rewards from expired reward rounds into the current reward round.
|
|
289
|
+
/// @param hook The hook whose expired reward rounds should be recycled.
|
|
290
|
+
/// @param token The reward token to recycle.
|
|
291
|
+
/// @param rounds The reward rounds to recycle.
|
|
292
|
+
/// @return amount The total amount recycled.
|
|
287
293
|
function burnExpiredRewards(address hook, IERC20 token, uint256[] calldata rounds) external returns (uint256 amount);
|
|
288
294
|
|
|
289
295
|
/// @notice Record the snapshot block for the current round. Callable by anyone (keepers, frontends).
|